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 07:33:01

            Line data    Source code
       1              : // SPDX-License-Identifier: LGPL-2.1-or-later
       2          283 : 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          283 : let systemd_client;
      92          283 : let systemd_manager;
      93              : 
      94          263 : function wait_valid(proxy, callback) {
      95          253 :     proxy.wait(() => {
      96          253 :         if (proxy.valid)
      97          253 :             callback();
      98          253 :     });
      99          263 : }
     100              : 
     101          263 : function with_systemd_manager(done) {
     102          263 :     if (!systemd_manager) {
     103              :         // cached forever, only used for reading/watching; no superuser
     104          263 :         systemd_client = cockpit.dbus("org.freedesktop.systemd1");
     105          263 :         systemd_manager = systemd_client.proxy("org.freedesktop.systemd1.Manager",
     106          263 :                                                "/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          263 :     }
     116          263 :     wait_valid(systemd_manager, done);
     117          263 : }
     118              : 
     119          263 : export function proxy(name, kind) {
     120          263 :     const self = {
     121          263 :         exists: null,
     122          263 :         state: null,
     123          263 :         enabled: null,
     124              : 
     125          263 :         wait,
     126              : 
     127          263 :         start,
     128          263 :         stop,
     129          263 :         restart,
     130          263 :         tryRestart,
     131              : 
     132          263 :         enable,
     133          263 :         disable,
     134              : 
     135          263 :         getRunJournal,
     136          263 :     };
     137              : 
     138          263 :     cockpit.event_target(self);
     139              : 
     140          263 :     let unit;
     141          263 :     let details;
     142          263 :     let wait_promise_resolve;
     143          263 :     const wait_promise = new Promise(resolve => { wait_promise_resolve = resolve });
     144              : 
     145          263 :     if (name.indexOf(".") == -1)
     146          177 :         name = name + ".service";
     147          263 :     if (kind === undefined)
     148          263 :         kind = "Service";
     149              : 
     150          246 :     function update_from_unit() {
     151          110 :         self.exists = (unit.LoadState != "not-found" || unit.ActiveState != "inactive");
     152              : 
     153          246 :         if (unit.ActiveState == "activating")
     154           64 :             self.state = "starting";
     155          246 :         else if (unit.ActiveState == "deactivating")
     156           45 :             self.state = "stopping";
     157          217 :         else if (unit.ActiveState == "active" || unit.ActiveState == "reloading")
     158          107 :             self.state = "running";
     159          217 :         else if (unit.ActiveState == "failed")
     160           37 :             self.state = "failed";
     161          217 :         else if (unit.ActiveState == "inactive" && self.exists)
     162          107 :             self.state = "stopped";
     163              :         else
     164          110 :             self.state = undefined;
     165              : 
     166          118 :         if (unit.UnitFileState == "enabled" || unit.UnitFileState == "linked")
     167          105 :             self.enabled = true;
     168          110 :         else if (unit.UnitFileState == "disabled" || unit.UnitFileState == "masked")
     169          107 :             self.enabled = false;
     170              :         else
     171          110 :             self.enabled = undefined;
     172              : 
     173          246 :         self.unit = unit;
     174              : 
     175          246 :         self.dispatchEvent("changed");
     176          246 :         wait_promise_resolve();
     177          246 :     }
     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          250 :                 .then(path => {
     188          250 :                     unit = systemd_client.proxy('org.freedesktop.systemd1.Unit', path);
     189          250 :                     unit.addEventListener('changed', update_from_unit);
     190          250 :                     wait_valid(unit, update_from_unit);
     191              : 
     192          250 :                     details = systemd_client.proxy('org.freedesktop.systemd1.' + kind, path);
     193          250 :                     details.addEventListener('changed', update_from_details);
     194          250 :                     wait_valid(details, update_from_details);
     195          250 :                 })
     196            0 :                 .catch(() => {
     197            0 :                     self.exists = false;
     198            0 :                     self.dispatchEvent('changed');
     199            0 :                 });
     200          253 :     });
     201              : 
     202           81 :     function refresh() {
     203           81 :         if (!unit || !details)
     204            4 :             return Promise.resolve();
     205              : 
     206           81 :         function refresh_interface(path, iface) {
     207           81 :             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           81 :         }
     216              : 
     217           81 :         return Promise.allSettled([
     218           81 :             refresh_interface(unit.path, "org.freedesktop.systemd1.Unit"),
     219           81 :             refresh_interface(details.path, "org.freedesktop.systemd1." + kind),
     220           81 :         ]);
     221           81 :     }
     222              : 
     223          140 :     function on_job_new_removed_refresh(event, number, path, unit_id, result) {
     224          140 :         if (unit_id == name)
     225           20 :             refresh();
     226          140 :     }
     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           80 :             refresh();
     253           81 :     });
     254              : 
     255          263 :     systemd_manager.addEventListener("JobNew", on_job_new_removed_refresh);
     256          263 :     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          263 :     return self;
     346          263 : }
        

Generated by: LCOV version 2.0-1