LCOV - code coverage report
Current view: top level - pkg/lib - service.js Coverage Total Hit
Test: cockpit Lines: 87.1 % 186 162
Test Date: 2026-07-17 12:03:54

            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          251 :     proxy.wait(() => {
      96          251 :         if (proxy.valid)
      97          251 :             callback();
      98          251 :     });
      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          251 :         wait_valid(systemd_manager, () => {
     108          251 :             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          251 :         });
     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          177 :         name = name + ".service";
     147          264 :     if (kind === undefined)
     148          264 :         kind = "Service";
     149              : 
     150          245 :     function update_from_unit() {
     151          110 :         self.exists = (unit.LoadState != "not-found" || unit.ActiveState != "inactive");
     152              : 
     153          245 :         if (unit.ActiveState == "activating")
     154           61 :             self.state = "starting";
     155          245 :         else if (unit.ActiveState == "deactivating")
     156           46 :             self.state = "stopping";
     157          219 :         else if (unit.ActiveState == "active" || unit.ActiveState == "reloading")
     158          111 :             self.state = "running";
     159          219 :         else if (unit.ActiveState == "failed")
     160           38 :             self.state = "failed";
     161          219 :         else if (unit.ActiveState == "inactive" && self.exists)
     162          108 :             self.state = "stopped";
     163              :         else
     164          110 :             self.state = undefined;
     165              : 
     166          120 :         if (unit.UnitFileState == "enabled" || unit.UnitFileState == "linked")
     167          108 :             self.enabled = true;
     168          110 :         else if (unit.UnitFileState == "disabled" || unit.UnitFileState == "masked")
     169          108 :             self.enabled = false;
     170              :         else
     171          110 :             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          251 :     with_systemd_manager(function () {
     186          251 :         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          251 :     });
     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           82 :                     .then(([result]) => {
     209           82 :                         const props = { };
     210           82 :                         for (const p in result)
     211           82 :                             props[p] = result[p].v;
     212           82 :                         systemd_client.notify({ [unit.path]: { [iface]: props } });
     213           82 :                     })
     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          138 :     function on_job_new_removed_refresh(event, number, path, unit_id, result) {
     224          138 :         if (unit_id == name)
     225           21 :             refresh();
     226          138 :     }
     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 : }
        

Generated by: LCOV version 2.0-1