LCOV - code coverage report
Current view: top level - pkg/lib - cockpit.js Coverage Total Hit
Test: cockpit Lines: 79.0 % 2220 1753
Test Date: 2026-07-13 10:00:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2014 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6              : /* eslint-disable indent,no-empty */
       7              : 
       8              : import { base64_encode, base64_decode } from './cockpit/_internal/base64';
       9              : import { Channel } from './cockpit/_internal/channel';
      10              : import {
      11              :     in_array, is_function, is_object, is_plain_object, invoke_functions, iterate_data, join_data
      12              : } from './cockpit/_internal/common';
      13              : import { Deferred, later_invoke } from './cockpit/_internal/deferred';
      14              : import { event_mixin } from './cockpit/_internal/event-mixin';
      15              : import { transport_origin, calculate_application, calculate_url } from './cockpit/_internal/location-utils';
      16              : import { Location } from 'cockpit/_internal/location';
      17              : import { ensure_transport, transport_globals } from './cockpit/_internal/transport';
      18              : import { FsInfoClient } from "./cockpit/fsinfo";
      19              : import { fetch_info } from './cockpit/_internal/info';
      20              : 
      21          368 : function factory() {
      22          368 :     const cockpit = { };
      23          368 :     event_mixin(cockpit, { });
      24              : 
      25           20 :     cockpit.init = async () => {
      26           20 :         await new Promise(resolve => ensure_transport(resolve));
      27           20 :         Object.assign(cockpit.info, await fetch_info());
      28           20 :     };
      29              : 
      30          368 :     cockpit.channel = function channel(options) {
      31          368 :         return new Channel(options);
      32          368 :     };
      33              : 
      34          366 :     cockpit.event_target = function event_target(obj) {
      35          366 :         event_mixin(obj, { });
      36          366 :         return obj;
      37          366 :     };
      38              : 
      39              :     /* obsolete backwards compatible shim */
      40          368 :     cockpit.extend = Object.assign;
      41              : 
      42              :     /* These can be filled in by loading ../manifests.js */
      43          368 :     cockpit.manifests = { };
      44              : 
      45              :     /* ------------------------------------------------------------
      46              :      * Text Encoding
      47              :      */
      48              : 
      49          368 :     cockpit.base64_encode = base64_encode;
      50          368 :     cockpit.base64_decode = base64_decode;
      51              : 
      52           66 :     cockpit.kill = function kill(host, group) {
      53           66 :         const options = { };
      54           66 :         if (host)
      55           19 :             options.host = host;
      56           66 :         if (group)
      57           66 :             options.group = group;
      58           66 :         cockpit.transport.control("kill", options);
      59           66 :     };
      60              : 
      61              :     /* Not public API ... yet? */
      62          163 :     cockpit.hint = function hint(name, options) {
      63          163 :         if (!transport_globals.default_transport)
      64          163 :             return;
      65          163 :         if (!options)
      66           11 :             options = transport_globals.default_host;
      67          163 :         if (typeof options == "string")
      68           11 :             options = { host: options };
      69          163 :         options.hint = name;
      70          163 :         cockpit.transport.control("hint", options);
      71          163 :     };
      72              : 
      73          368 :     cockpit.transport = {
      74          368 :         wait: ensure_transport,
      75          336 :         inject: function inject(message, out) {
      76          336 :             if (!transport_globals.default_transport)
      77           61 :                 return false;
      78          336 :             if (out === undefined || out)
      79           61 :                 return transport_globals.default_transport.send_data(message);
      80              :             else
      81           61 :                 return transport_globals.default_transport.dispatch_data({ data: message });
      82          336 :         },
      83          341 :         filter: function filter(callback, out) {
      84           63 :             if (out) {
      85           63 :                 console.error("'out' filters are no longer supported");
      86           63 :             } else {
      87          341 :                 transport_globals.incoming_filters.push(callback);
      88          341 :             }
      89          341 :         },
      90            0 :         close: function close(problem) {
      91            0 :             if (transport_globals.default_transport)
      92            0 :                 transport_globals.default_transport.close(problem ? { problem } : undefined);
      93            0 :             transport_globals.default_transport = null;
      94            0 :             this.options = { };
      95            0 :         },
      96          368 :         origin: transport_origin,
      97          368 :         options: { },
      98          368 :         uri: calculate_url,
      99          239 :         control: function(command, options) {
     100          239 :             options = { ...options, command };
     101          239 :             ensure_transport(function(transport) {
     102          239 :                 transport.send_control(options);
     103          239 :             });
     104          239 :         },
     105          341 :         application: function () {
     106          341 :             if (!transport_globals.default_transport || window.mock)
     107           63 :                 return calculate_application();
     108          341 :             return transport_globals.default_transport.application;
     109          341 :         },
     110          368 :     };
     111              : 
     112            0 :     cockpit.resolve = function resolve(result) {
     113            0 :         console.warn("cockpit.resolve() is deprecated. Use Promise.resolve()");
     114            0 :         return Promise.resolve(result);
     115            0 :     };
     116              : 
     117            0 :     cockpit.reject = function reject(ex) {
     118            0 :         console.warn("cockpit.reject() is deprecated. Use Promise.reject()");
     119            0 :         return Promise.reject(ex);
     120            0 :     };
     121              : 
     122          368 :     cockpit.defer = function() {
     123          368 :         return new Deferred();
     124          368 :     };
     125              : 
     126              :     /* ---------------------------------------------------------------------
     127              :      * Utilities
     128              :      */
     129              : 
     130          368 :     const fmt_re = /\$\{([^}]+)\}|\$([a-zA-Z0-9_]+)/g;
     131          363 :     cockpit.format = function format(fmt, args) {
     132          129 :         if (arguments.length != 2 || !is_object(args) || args === null)
     133          363 :             args = Array.prototype.slice.call(arguments, 1);
     134              : 
     135          363 :         function replace(m, x, y) {
     136          363 :             const value = args[x || y];
     137              : 
     138              :             /* Special-case 0 (also catches 0.0). All other falsy values return
     139              :              * the empty string.
     140              :              */
     141          363 :             if (value === 0)
     142           88 :                 return '0';
     143              : 
     144          166 :             return value || '';
     145          363 :         }
     146              : 
     147          363 :         return fmt.replace(fmt_re, replace);
     148          363 :     };
     149              : 
     150          206 :     cockpit.format_number = function format_number(number, precision) {
     151              :         /* We show given number of digits of precision (default 3), but avoid scientific notation.
     152              :          * We also show integers without digits after the comma.
     153              :          *
     154              :          * We want to localise the decimal separator, but we never want to
     155              :          * show thousands separators (to avoid ambiguity).  For this
     156              :          * reason, for integers and large enough numbers, we use
     157              :          * non-localised conversions (and in both cases, show no
     158              :          * fractional part).
     159              :          */
     160          206 :         if (precision === undefined)
     161          179 :             precision = 3;
     162           26 :         const lang = cockpit.language === undefined ? undefined : cockpit.language.replace('_', '-');
     163          206 :         const smallestValue = 10 ** (-precision);
     164              : 
     165          156 :         if (!number && number !== 0)
     166           65 :             return "";
     167          206 :         else if (number % 1 === 0)
     168          140 :             return number.toString();
     169          197 :         else if (number > 0 && number <= smallestValue)
     170           26 :             return smallestValue.toLocaleString(lang);
     171           26 :         else if (number < 0 && number >= -smallestValue)
     172           26 :             return (-smallestValue).toLocaleString(lang);
     173          197 :         else if (number > 999 || number < -999)
     174           33 :             return number.toFixed(0);
     175              :         else
     176          197 :             return number.toLocaleString(lang, {
     177          197 :                 maximumSignificantDigits: precision,
     178          197 :                 minimumSignificantDigits: precision,
     179          197 :             });
     180          206 :     };
     181              : 
     182          368 :     let deprecated_format_warned = false;
     183          206 :     function format_units(suffixes, number, second_arg, third_arg) {
     184          206 :         let options = second_arg;
     185          112 :         let factor = options?.base2 ? 1024 : 1000;
     186              : 
     187              :         // compat API: we used to accept 'factor' as a separate second arg
     188          145 :         if (third_arg || (second_arg && !is_object(second_arg))) {
     189          175 :             if (!deprecated_format_warned) {
     190          175 :                 console.warn(`cockpit.format_{bytes,bits}[_per_sec](..., ${second_arg}, ${third_arg}) is deprecated.`);
     191          175 :                 deprecated_format_warned = true;
     192          175 :             }
     193              : 
     194           26 :             factor = second_arg || 1000;
     195          175 :             options = third_arg;
     196              :             // double backwards compat: "options" argument position used to be a boolean flag "separate"
     197          175 :             if (!is_object(options))
     198          145 :                 options = { separate: options };
     199          175 :         }
     200              : 
     201          206 :         let suffix = null;
     202              : 
     203              :         /* Find that factor string */
     204           65 :         if (!number && number !== 0) {
     205           65 :             suffix = null;
     206           56 :         } else if (typeof (factor) === "string") {
     207              :             /* Prefer larger factors */
     208          175 :             const keys = [];
     209          175 :             for (const key in suffixes)
     210          175 :                 keys.push(key);
     211          175 :             keys.sort().reverse();
     212          175 :             for (let y = 0; y < keys.length; y++) {
     213          175 :                 for (let x = 0; x < suffixes[keys[y]].length; x++) {
     214          175 :                     if (factor == suffixes[keys[y]][x]) {
     215          175 :                         number = number / Math.pow(keys[y], x);
     216          175 :                         suffix = factor;
     217          175 :                         break;
     218          175 :                     }
     219          175 :                 }
     220          175 :                 if (suffix)
     221          175 :                     break;
     222          175 :             }
     223              : 
     224              :         /* @factor is a number */
     225          175 :         } else if (factor in suffixes) {
     226          206 :             let divisor = 1;
     227          206 :             for (let i = 0; i < suffixes[factor].length; i++) {
     228          206 :                 const quotient = number / divisor;
     229          206 :                 if (quotient < factor) {
     230          206 :                     number = quotient;
     231          206 :                     suffix = suffixes[factor][i];
     232          206 :                     break;
     233          206 :                 }
     234          205 :                 divisor *= factor;
     235          205 :             }
     236          206 :         }
     237              : 
     238          176 :         const string_representation = cockpit.format_number(number, options?.precision);
     239          206 :         let ret;
     240              : 
     241          206 :         if (string_representation && suffix)
     242           65 :             ret = [string_representation, suffix];
     243              :         else
     244           65 :             ret = [string_representation];
     245              : 
     246          176 :         if (!options?.separate)
     247          178 :             ret = ret.join(" ");
     248              : 
     249          206 :         return ret;
     250          206 :     }
     251              : 
     252          368 :     const byte_suffixes = {
     253          368 :         1000: ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB"],
     254          368 :         1024: ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"]
     255          368 :     };
     256              : 
     257          173 :     cockpit.format_bytes = function format_bytes(number, ...args) {
     258          173 :         return format_units(byte_suffixes, number, ...args);
     259          173 :     };
     260              : 
     261          368 :     const byte_sec_suffixes = {
     262          368 :         1000: ["B/s", "kB/s", "MB/s", "GB/s", "TB/s", "PB/s", "EB/s", "ZB/s"],
     263          368 :         1024: ["B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s", "PiB/s", "EiB/s", "ZiB/s"]
     264          368 :     };
     265              : 
     266          103 :     cockpit.format_bytes_per_sec = function format_bytes_per_sec(number, ...args) {
     267          103 :         return format_units(byte_sec_suffixes, number, ...args);
     268          103 :     };
     269              : 
     270          368 :     const bit_suffixes = {
     271          368 :         1000: ["bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps"]
     272          368 :     };
     273              : 
     274           37 :     cockpit.format_bits_per_sec = function format_bits_per_sec(number, ...args) {
     275           37 :         return format_units(bit_suffixes, number, ...args);
     276           37 :     };
     277              : 
     278              :     /* ---------------------------------------------------------------------
     279              :      * Storage Helper.
     280              :      *
     281              :      * Use application to prefix data stored in browser storage
     282              :      * with helpers for compatibility.
     283              :      */
     284          368 :     function StorageHelper(storageName) {
     285          368 :         const self = this;
     286          368 :         let storage;
     287              : 
     288          368 :         try {
     289          368 :             storage = window[storageName];
     290           67 :         } catch (e) { }
     291              : 
     292          341 :         self.prefixedKey = function (key) {
     293          341 :             return cockpit.transport.application() + ":" + key;
     294          341 :         };
     295              : 
     296            1 :         self.getItem = function (key, both) {
     297            1 :             let value = storage.getItem(self.prefixedKey(key));
     298            1 :             if (!value && both)
     299            1 :                 value = storage.getItem(key);
     300            1 :             return value;
     301            1 :         };
     302              : 
     303            0 :         self.setItem = function (key, value, both) {
     304            0 :             storage.setItem(self.prefixedKey(key), value);
     305            0 :             if (both)
     306            0 :                 storage.setItem(key, value);
     307            0 :         };
     308              : 
     309            0 :         self.removeItem = function(key, both) {
     310            0 :             storage.removeItem(self.prefixedKey(key));
     311            0 :             if (both)
     312            0 :                 storage.removeItem(key);
     313            0 :         };
     314              : 
     315              :         /* Instead of clearing, purge anything that isn't prefixed with an application
     316              :          * and anything prefixed with our application.
     317              :          */
     318            0 :         self.clear = function(full) {
     319            0 :             let i = 0;
     320            0 :             while (i < storage.length) {
     321            0 :                 const k = storage.key(i);
     322            0 :                 if (full && k.indexOf("cockpit") !== 0)
     323            0 :                     storage.removeItem(k);
     324            0 :                 else if (k.indexOf(cockpit.transport.application()) === 0)
     325            0 :                     storage.removeItem(k);
     326              :                 else
     327            0 :                     i++;
     328            0 :             }
     329            0 :         };
     330          368 :     }
     331              : 
     332          368 :     cockpit.localStorage = new StorageHelper("localStorage");
     333          368 :     cockpit.sessionStorage = new StorageHelper("sessionStorage");
     334              : 
     335              :     /* ---------------------------------------------------------------------
     336              :      * Shared data cache.
     337              :      *
     338              :      * We cannot use sessionStorage when keeping lots of data in memory and
     339              :      * sharing it between frames. It has a rather paltry limit on the amount
     340              :      * of data it can hold ... so we use window properties instead.
     341              :      */
     342              : 
     343            0 :     function lookup_storage(win) {
     344            0 :         let storage;
     345            0 :         if (win.parent && win.parent !== win)
     346            0 :             storage = lookup_storage(win.parent);
     347            0 :         if (!storage) {
     348            0 :             try {
     349            0 :                 storage = win["cv1-storage"];
     350            0 :                 if (!storage)
     351            0 :                     win["cv1-storage"] = storage = { };
     352            0 :             } catch (ex) { }
     353            0 :         }
     354            0 :         return storage;
     355            0 :     }
     356              : 
     357            0 :     function StorageCache(org_key, provider, consumer) {
     358            0 :         const self = this;
     359            0 :         const key = cockpit.transport.application() + ":" + org_key;
     360              : 
     361              :         /* For triggering events and ownership */
     362            0 :         const trigger = window.sessionStorage;
     363            0 :         let last;
     364              : 
     365            0 :         const storage = lookup_storage(window);
     366              : 
     367            0 :         let claimed = false;
     368            0 :         let source;
     369              : 
     370            0 :         function callback() {
     371              :             /* Only run the callback if we have a result */
     372            0 :             if (storage[key] !== undefined) {
     373            0 :                 const value = storage[key];
     374            0 :                 window.setTimeout(function() {
     375            0 :                     if (consumer(value, org_key) === false)
     376            0 :                         self.close();
     377            0 :                 });
     378            0 :             }
     379            0 :         }
     380              : 
     381            0 :         function result(value) {
     382            0 :             if (source && !claimed)
     383            0 :                 claimed = true;
     384            0 :             if (!claimed)
     385            0 :                 return;
     386              : 
     387              :             // use a random number to avoid races by separate instances
     388            0 :             const version = Math.floor(Math.random() * 10000000) + 1;
     389              : 
     390              :             /* Event for the local window */
     391            0 :             const ev = document.createEvent("StorageEvent");
     392            0 :             ev.initStorageEvent("storage", false, false, key, null,
     393            0 :                                 version, window.location, trigger);
     394              : 
     395            0 :             storage[key] = value;
     396            0 :             trigger.setItem(key, version);
     397            0 :             ev.self = self;
     398            0 :             window.dispatchEvent(ev);
     399            0 :         }
     400              : 
     401            0 :         self.claim = function claim() {
     402            0 :             if (source)
     403            0 :                 return;
     404              : 
     405              :             /* In case we're unclaimed during the callback */
     406            0 :             const claiming = { close: function() { } };
     407            0 :             source = claiming;
     408              : 
     409            0 :             const changed = provider(result, org_key);
     410            0 :             if (source === claiming)
     411            0 :                 source = changed;
     412              :             else
     413            0 :                 changed.close();
     414            0 :         };
     415              : 
     416            0 :         function unclaim() {
     417            0 :             if (source?.close)
     418            0 :                 source.close();
     419            0 :             source = null;
     420              : 
     421            0 :             if (!claimed)
     422            0 :                 return;
     423              : 
     424            0 :             claimed = false;
     425              : 
     426            0 :             let current_value = trigger.getItem(key);
     427            0 :             if (current_value)
     428            0 :                 current_value = parseInt(current_value, 10);
     429              :             else
     430            0 :                 current_value = null;
     431              : 
     432            0 :             if (last && last === current_value) {
     433            0 :                 const ev = document.createEvent("StorageEvent");
     434            0 :                 const version = trigger[key];
     435            0 :                 ev.initStorageEvent("storage", false, false, key, version,
     436            0 :                                     null, window.location, trigger);
     437            0 :                 delete storage[key];
     438            0 :                 trigger.removeItem(key);
     439            0 :                 ev.self = self;
     440            0 :                 window.dispatchEvent(ev);
     441            0 :             }
     442            0 :         }
     443              : 
     444            0 :         function changed(event) {
     445            0 :             if (event.key !== key)
     446            0 :                 return;
     447              : 
     448              :             /* check where the event came from
     449              :                - it came from someone else:
     450              :                    if it notifies their unclaim (new value null) and we haven't already claimed, do so
     451              :                - it came from ourselves:
     452              :                    if the new value doesn't match the actual value in the cache, and
     453              :                    we tried to claim (from null to a number), cancel our claim
     454              :              */
     455            0 :             if (event.self !== self) {
     456            0 :                 if (!event.newValue && !claimed) {
     457            0 :                     self.claim();
     458            0 :                     return;
     459            0 :                 }
     460            0 :             } else if (claimed && !event.oldValue && (event.newValue !== trigger.getItem(key))) {
     461            0 :                 unclaim();
     462            0 :             }
     463              : 
     464            0 :             let new_value = null;
     465            0 :             if (event.newValue)
     466            0 :                 new_value = parseInt(event.newValue, 10);
     467            0 :             if (last !== new_value) {
     468            0 :                 last = new_value;
     469            0 :                 callback();
     470            0 :             }
     471            0 :         }
     472              : 
     473            0 :         self.close = function() {
     474            0 :             window.removeEventListener("storage", changed, true);
     475            0 :             unclaim();
     476            0 :         };
     477              : 
     478            0 :         window.addEventListener("storage", changed, true);
     479              : 
     480              :         /* Always clear this data on unload */
     481            0 :         window.addEventListener("beforeunload", function() {
     482            0 :             self.close();
     483            0 :         });
     484            0 :         window.addEventListener("unload", function() {
     485            0 :             self.close();
     486            0 :         });
     487              : 
     488            0 :         if (trigger.getItem(key))
     489            0 :             callback();
     490              :         else
     491            0 :             self.claim();
     492            0 :     }
     493              : 
     494            0 :     cockpit.cache = function cache(key, provider, consumer) {
     495            0 :         return new StorageCache(key, provider, consumer);
     496            0 :     };
     497              : 
     498              :     /* ---------------------------------------------------------------------
     499              :      * Metrics
     500              :      *
     501              :      * Implements the cockpit.series and cockpit.grid. Part of the metrics
     502              :      * implementations that do not require jquery.
     503              :      */
     504              : 
     505          149 :     function SeriesSink(interval, identifier, fetch_callback) {
     506          149 :         const self = this;
     507              : 
     508          149 :         self.interval = interval;
     509           20 :         self.limit = identifier ? 64 * 1024 : 1024;
     510              : 
     511              :         /*
     512              :          * The cache sits on a window, either our own or a parent
     513              :          * window whichever we can access properly.
     514              :          *
     515              :          * Entries in the index are:
     516              :          *
     517              :          * { beg: N, items: [], mapping: { }, next: item }
     518              :          */
     519          149 :         const index = setup_index(identifier);
     520              : 
     521              :         /*
     522              :          * A linked list through the index, that we use for expiry
     523              :          * of the cache.
     524              :          */
     525          149 :         let count = 0;
     526          149 :         let head = null;
     527          149 :         let tail = null;
     528              : 
     529          149 :         function setup_index(id) {
     530          149 :             if (!id)
     531          149 :                 return [];
     532              : 
     533              :             /* Try and find a good place to cache data */
     534           20 :             const storage = lookup_storage(window);
     535              : 
     536           20 :             let index = storage[id];
     537           20 :             if (!index)
     538           20 :                 storage[id] = index = [];
     539           20 :             return index;
     540          149 :         }
     541              : 
     542          149 :         function search(idx, beg) {
     543          149 :             let low = 0;
     544          149 :             let high = idx.length - 1;
     545              : 
     546          149 :             while (low <= high) {
     547          149 :                 const mid = (low + high) / 2 | 0;
     548          149 :                 const val = idx[mid].beg;
     549          149 :                 if (val < beg)
     550          143 :                     low = mid + 1;
     551          149 :                 else if (val > beg)
     552          138 :                     high = mid - 1;
     553              :                 else
     554          148 :                     return mid; /* key found */
     555          149 :             }
     556          149 :             return low;
     557          149 :         }
     558              : 
     559          149 :         function fetch(beg, end, for_walking) {
     560          149 :             if (fetch_callback) {
     561          149 :                 if (!for_walking) {
     562              :                     /* Stash some fake data synchronously so that we don't ask
     563              :                      * again for the same range while they are still fetching
     564              :                      * it asynchronously.
     565              :                      */
     566          149 :                     stash(beg, new Array(end - beg), { });
     567          149 :                 }
     568          149 :                 fetch_callback(beg, end, for_walking);
     569          149 :             }
     570          149 :         }
     571              : 
     572          149 :         self.load = function load(beg, end, for_walking) {
     573          149 :             if (end <= beg)
     574          149 :                 return;
     575              : 
     576          149 :             const at = search(index, beg);
     577              : 
     578          149 :             const len = index.length;
     579          149 :             let last = beg;
     580              : 
     581              :             /* We do this in two phases: First, we walk the index to
     582              :              * process what we already have and at the same time make
     583              :              * notes about what we need to fetch.  Then we go over the
     584              :              * notes and actually fetch what we need.  That way, the
     585              :              * fetch callbacks in the second phase can modify the
     586              :              * index data structure without disturbing the walk in the
     587              :              * first phase.
     588              :              */
     589              : 
     590          149 :             const fetches = [];
     591              : 
     592              :             /* Data relevant to this range can be at the found index, or earlier */
     593          142 :             for (let i = at > 0 ? at - 1 : at; i < len; i++) {
     594          149 :                 const entry = index[i];
     595          149 :                 const en = entry.items.length;
     596          149 :                 if (!en)
     597          149 :                     continue;
     598              : 
     599          149 :                 const eb = entry.beg;
     600          149 :                 const b = Math.max(eb, beg);
     601          149 :                 const e = Math.min(eb + en, end);
     602              : 
     603          149 :                 if (b < e) {
     604          149 :                     if (b > last)
     605          130 :                         fetches.push([last, b]);
     606          149 :                     process(b, entry.items.slice(b - eb, e - eb), entry.mapping);
     607          149 :                     last = e;
     608          102 :                 } else if (i >= at) {
     609          102 :                     break; /* no further intersections */
     610          102 :                 }
     611          149 :             }
     612              : 
     613          149 :             for (let i = 0; i < fetches.length; i++)
     614          130 :                 fetch(fetches[i][0], fetches[i][1], for_walking);
     615              : 
     616          149 :             if (last != end)
     617          149 :                 fetch(last, end, for_walking);
     618          149 :         };
     619              : 
     620          149 :         function stash(beg, items, mapping) {
     621          149 :             if (!items.length)
     622          149 :                 return;
     623              : 
     624          149 :             let at = search(index, beg);
     625              : 
     626          149 :             const end = beg + items.length;
     627              : 
     628          149 :             const len = index.length;
     629          149 :             let i;
     630          143 :             for (i = at > 0 ? at - 1 : at; i < len; i++) {
     631          143 :                 const entry = index[i];
     632          143 :                 const en = entry.items.length;
     633          143 :                 if (!en)
     634          143 :                     continue;
     635              : 
     636          143 :                 const eb = entry.beg;
     637          143 :                 const b = Math.max(eb, beg);
     638          143 :                 const e = Math.min(eb + en, end);
     639              : 
     640              :                 /*
     641              :                  * We truncate blocks that intersect with this one
     642              :                  *
     643              :                  * We could adjust them, but in general the loaders are
     644              :                  * intelligent enough to only load the required data, so
     645              :                  * not doing this optimization yet.
     646              :                  */
     647              : 
     648           43 :                 if (b < e) {
     649           43 :                     const num = e - b;
     650           43 :                     entry.items.splice(b - eb, num);
     651           43 :                     count -= num;
     652           43 :                     if (b - eb === 0)
     653           39 :                         entry.beg += (e - eb);
     654           28 :                 } else if (i >= at) {
     655           54 :                     break; /* no further intersections */
     656           54 :                 }
     657          143 :             }
     658              : 
     659              :             /* Insert our item into the array */
     660          149 :             const entry = { beg, items, mapping };
     661          149 :             if (!head)
     662          149 :                 head = entry;
     663          149 :             if (tail)
     664          143 :                 tail.next = entry;
     665          149 :             tail = entry;
     666          149 :             count += items.length;
     667          149 :             index.splice(at, 0, entry);
     668              : 
     669              :             /* Remove any items with zero length around insertion point */
     670          149 :             for (at--; at <= i; at++) {
     671          149 :                 const entry = index[at];
     672           27 :                 if (entry && !entry.items.length) {
     673           27 :                     index.splice(at, 1);
     674           27 :                     at--;
     675           27 :                 }
     676          149 :             }
     677              : 
     678              :             /* If our index has gotten too big, expire entries */
     679           20 :             while (head && count > self.limit) {
     680           20 :                 count -= head.items.length;
     681           20 :                 head.items = [];
     682           20 :                 head.mapping = null;
     683           20 :                 head = head.next || null;
     684           20 :             }
     685              : 
     686              :             /* Remove any entries with zero length at beginning */
     687          149 :             const newlen = index.length;
     688          149 :             for (i = 0; i < newlen; i++) {
     689          149 :                 if (index[i].items.length > 0)
     690          149 :                     break;
     691          149 :             }
     692          149 :             index.splice(0, i);
     693          149 :         }
     694              : 
     695              :         /*
     696              :          * Used to populate grids, the keys are grid ids and
     697              :          * the values are objects: { grid, rows, notify }
     698              :          *
     699              :          * The rows field is an object indexed by paths
     700              :          * container aliases, and the values are: [ row, path ]
     701              :          */
     702          149 :         const registered = { };
     703              : 
     704              :         /* An undocumented function called by DataGrid */
     705          149 :         self._register = function _register(grid, id) {
     706          149 :             if (grid.interval != interval)
     707           20 :                 throw Error("mismatched metric interval between grid and sink");
     708          149 :             let gdata = registered[id];
     709          149 :             if (!gdata) {
     710          149 :                 gdata = registered[id] = { grid, links: [] };
     711            1 :                 gdata.links.remove = function remove() {
     712            1 :                     delete registered[id];
     713            1 :                 };
     714          149 :             }
     715          149 :             return gdata.links;
     716          149 :         };
     717              : 
     718          149 :         function process(beg, items, mapping) {
     719          149 :             const end = beg + items.length;
     720              : 
     721          149 :             for (const id in registered) {
     722          149 :                 const gdata = registered[id];
     723          149 :                 const grid = gdata.grid;
     724              : 
     725          149 :                 const b = Math.max(beg, grid.beg);
     726          149 :                 const e = Math.min(end, grid.end);
     727              : 
     728              :                 /* Does this grid overlap the bounds of item? */
     729          149 :                 if (b < e) {
     730              :                     /* Where in the items to take from */
     731          149 :                     const f = b - beg;
     732              : 
     733              :                     /* Where and how many to place */
     734          149 :                     const t = b - grid.beg;
     735              : 
     736              :                     /* How many to process */
     737          149 :                     const n = e - b;
     738              : 
     739          149 :                     for (let i = 0; i < n; i++) {
     740          149 :                         const klen = gdata.links.length;
     741          149 :                         for (let k = 0; k < klen; k++) {
     742          149 :                             const path = gdata.links[k][0];
     743          149 :                             const row = gdata.links[k][1];
     744              : 
     745              :                             /* Calculate the data field to fill in */
     746          149 :                             let data = items[f + i];
     747          149 :                             let map = mapping;
     748          149 :                             const jlen = path.length;
     749          136 :                             for (let j = 0; data !== undefined && j < jlen; j++) {
     750           20 :                                 if (!data) {
     751           20 :                                     data = undefined;
     752           20 :                                 } else if (map !== undefined && map !== null) {
     753          136 :                                     map = map[path[j]];
     754          136 :                                     if (map)
     755          130 :                                         data = data[map[""]];
     756              :                                     else
     757          130 :                                         data = data[path[j]];
     758           20 :                                 } else {
     759           20 :                                     data = data[path[j]];
     760           20 :                                 }
     761          136 :                             }
     762              : 
     763          149 :                             row[t + i] = data;
     764          149 :                         }
     765          149 :                     }
     766              : 
     767              :                     /* Notify the grid, so it can call any functions */
     768          149 :                     grid.notify(t, n);
     769          149 :                 }
     770          149 :             }
     771          149 :         }
     772              : 
     773          139 :         self.input = function input(beg, items, mapping) {
     774          139 :             process(beg, items, mapping);
     775          139 :             stash(beg, items, mapping);
     776          139 :         };
     777              : 
     778            1 :         self.close = function () {
     779            1 :             for (const id in registered) {
     780            1 :                 const grid = registered[id];
     781            1 :                 if (grid?.grid)
     782            1 :                     grid.grid.remove_sink(self);
     783            1 :             }
     784            1 :         };
     785          149 :     }
     786              : 
     787          149 :     cockpit.series = function series(interval, cache, fetch) {
     788          149 :         return new SeriesSink(interval, cache, fetch);
     789          149 :     };
     790              : 
     791          368 :     let unique = 1;
     792              : 
     793          149 :     function SeriesGrid(interval, beg, end) {
     794          149 :         const self = this;
     795              : 
     796              :         /* We can trigger events */
     797          149 :         event_mixin(self, { });
     798              : 
     799          149 :         const rows = [];
     800              : 
     801          149 :         self.interval = interval;
     802          149 :         self.beg = 0;
     803          149 :         self.end = 0;
     804              : 
     805              :         /*
     806              :          * Used to populate table data, the values are:
     807              :          * [ callback, row ]
     808              :          */
     809          149 :         const callbacks = [];
     810              : 
     811          149 :         const sinks = [];
     812              : 
     813          149 :         let suppress = 0;
     814              : 
     815          149 :         const id = "g1-" + unique;
     816          149 :         unique += 1;
     817              : 
     818              :         /* Used while walking */
     819          149 :         let walking = null;
     820          149 :         let offset = null;
     821              : 
     822          149 :         self.notify = function notify(x, n) {
     823          149 :             if (suppress)
     824          149 :                 return;
     825          149 :             if (x + n > self.end - self.beg)
     826           20 :                 n = (self.end - self.beg) - x;
     827          149 :             if (n <= 0)
     828          149 :                 return;
     829          149 :             const jlen = callbacks.length;
     830          149 :             for (let j = 0; j < jlen; j++) {
     831          149 :                 const callback = callbacks[j][0];
     832          149 :                 const row = callbacks[j][1];
     833          149 :                 callback.call(self, row, x, n);
     834          149 :             }
     835              : 
     836          149 :             self.dispatchEvent("notify", x, n);
     837          149 :         };
     838              : 
     839          149 :         self.add = function add(/* sink, path */) {
     840          149 :             const row = [];
     841          149 :             rows.push(row);
     842              : 
     843              :             /* Called as add(sink, path) */
     844          149 :             if (is_object(arguments[0])) {
     845           20 :                 const sink = arguments[0].series || arguments[0];
     846              : 
     847              :                 /* The path argument can be an array, or a dot separated string */
     848          149 :                 let path = arguments[1];
     849          149 :                 if (!path)
     850           20 :                     path = [];
     851          149 :                 else if (typeof (path) === "string")
     852           20 :                     path = path.split(".");
     853              : 
     854          149 :                 const links = sink._register(self, id);
     855          149 :                 if (!links.length)
     856          149 :                     sinks.push({ sink, links });
     857          149 :                 links.push([path, row]);
     858              : 
     859              :             /* Called as add(callback) */
     860          149 :             } else if (is_function(arguments[0])) {
     861          149 :                 const cb = [arguments[0], row];
     862          149 :                 if (arguments[1] === true)
     863           20 :                     callbacks.unshift(cb);
     864              :                 else
     865          149 :                     callbacks.push(cb);
     866              : 
     867              :             /* Not called as add() */
     868           20 :             } else if (arguments.length !== 0) {
     869           20 :                 throw Error("invalid args to grid.add()");
     870           20 :             }
     871              : 
     872          149 :             return row;
     873          149 :         };
     874              : 
     875            8 :         self.remove = function remove(row) {
     876              :             /* Remove from the sinks */
     877            8 :             let ilen = sinks.length;
     878            8 :             for (let i = 0; i < ilen; i++) {
     879            8 :                 const jlen = sinks[i].links.length;
     880            8 :                 for (let j = 0; j < jlen; j++) {
     881            8 :                     if (sinks[i].links[j][1] === row) {
     882            8 :                         sinks[i].links.splice(j, 1);
     883            8 :                         break;
     884            8 :                     }
     885            8 :                 }
     886            8 :             }
     887              : 
     888              :             /* Remove from our list of rows */
     889            8 :             ilen = rows.length;
     890            8 :             for (let i = 0; i < ilen; i++) {
     891            8 :                 if (rows[i] === row) {
     892            8 :                     rows.splice(i, 1);
     893            8 :                     break;
     894            8 :                 }
     895            8 :             }
     896            8 :         };
     897              : 
     898            1 :         self.remove_sink = function remove_sink(sink) {
     899            1 :             const len = sinks.length;
     900            1 :             for (let i = 0; i < len; i++) {
     901            1 :                 if (sinks[i].sink === sink) {
     902            1 :                     sinks[i].links.remove();
     903            1 :                     sinks.splice(i, 1);
     904            1 :                     break;
     905            1 :                 }
     906            1 :             }
     907            1 :         };
     908              : 
     909          149 :         self.sync = function sync(for_walking) {
     910              :             /* Suppress notifications */
     911          149 :             suppress++;
     912              : 
     913              :             /* Ask all sinks to load data */
     914          149 :             const len = sinks.length;
     915          149 :             for (let i = 0; i < len; i++) {
     916          149 :                 const sink = sinks[i].sink;
     917          149 :                 sink.load(self.beg, self.end, for_walking);
     918          149 :             }
     919              : 
     920          149 :             suppress--;
     921              : 
     922              :             /* Notify for all rows */
     923          149 :             self.notify(0, self.end - self.beg);
     924          149 :         };
     925              : 
     926          149 :         function move_internal(beg, end, for_walking) {
     927          149 :             if (end === undefined)
     928          143 :                 end = beg + (self.end - self.beg);
     929              : 
     930          149 :             if (end < beg)
     931           20 :                 beg = end;
     932              : 
     933          149 :             self.beg = beg;
     934          149 :             self.end = end;
     935              : 
     936          149 :             if (!rows.length)
     937          149 :                 return;
     938              : 
     939          136 :             rows.forEach(function(row) {
     940          136 :                 row.length = 0;
     941          136 :             });
     942              : 
     943          142 :             self.sync(for_walking);
     944          149 :         }
     945              : 
     946          149 :         function stop_walking() {
     947          149 :             window.clearInterval(walking);
     948          149 :             walking = null;
     949          149 :             offset = null;
     950          149 :         }
     951              : 
     952          149 :         function is_negative(n) {
     953          149 :             return ((n = +n) || 1 / n) < 0;
     954          149 :         }
     955              : 
     956          149 :         self.move = function move(beg, end) {
     957          149 :             stop_walking();
     958              :             /* Some code paths use now twice.
     959              :              * They should use the same value.
     960              :              */
     961          149 :             let now = null;
     962              : 
     963              :             /* Treat negative numbers relative to now */
     964           20 :             if (beg === undefined) {
     965           20 :                 beg = 0;
     966           20 :             } else if (is_negative(beg)) {
     967          149 :                 now = Date.now();
     968          149 :                 beg = Math.floor(now / self.interval) + beg;
     969          149 :             }
     970          149 :             if (end !== undefined && is_negative(end)) {
     971          149 :                 if (now === null)
     972           20 :                     now = Date.now();
     973          149 :                 end = Math.floor(now / self.interval) + end;
     974          149 :             }
     975              : 
     976          149 :             move_internal(beg, end, false);
     977          149 :         };
     978              : 
     979          149 :         self.walk = function walk() {
     980              :             /* Don't overflow 32 signed bits with the interval since
     981              :              * many browsers will mishandle it.  This means that plots
     982              :              * that would make about one step every month don't walk
     983              :              * at all, but I guess that is ok.
     984              :              *
     985              :              * For example,
     986              :              * https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
     987              :              * says:
     988              :              *
     989              :              *    Browsers including Internet Explorer, Chrome,
     990              :              *    Safari, and Firefox store the delay as a 32-bit
     991              :              *    signed Integer internally. This causes an Integer
     992              :              *    overflow when using delays larger than 2147483647,
     993              :              *    resulting in the timeout being executed immediately.
     994              :              */
     995              : 
     996          149 :             const start = Date.now();
     997          149 :             if (self.interval > 2000000000)
     998          149 :                 return;
     999              : 
    1000          149 :             stop_walking();
    1001          149 :             offset = start - self.beg * self.interval;
    1002          137 :             walking = window.setInterval(function() {
    1003          137 :                 const now = Date.now();
    1004          137 :                 move_internal(Math.floor((now - offset) / self.interval), undefined, true);
    1005          137 :             }, self.interval);
    1006          149 :         };
    1007              : 
    1008            0 :         self.close = function close() {
    1009            0 :             stop_walking();
    1010            0 :             while (sinks.length)
    1011            0 :                 (sinks.pop()).links.remove();
    1012            0 :         };
    1013              : 
    1014          149 :         self.move(beg, end);
    1015          149 :     }
    1016              : 
    1017          149 :     cockpit.grid = function grid(interval, beg, end) {
    1018          149 :         return new SeriesGrid(interval, beg, end);
    1019          149 :     };
    1020              : 
    1021              :     /* --------------------------------------------------------------------
    1022              :      * Basic utilities.
    1023              :      */
    1024              : 
    1025            3 :     function BasicError(problem, message) {
    1026            3 :         this.problem = problem;
    1027            1 :         this.message = message || cockpit.message(problem);
    1028            0 :         this.toString = function() {
    1029            0 :             return this.message;
    1030            0 :         };
    1031            3 :     }
    1032              : 
    1033            0 :     cockpit.logout = function logout(reload, reason) {
    1034              :         /* fully clear session storage */
    1035            0 :         cockpit.sessionStorage.clear(true);
    1036              : 
    1037              :         /* Only clean application data from localStorage,
    1038              :          * except for login-data. Clear that completely */
    1039            0 :         cockpit.localStorage.removeItem('login-data', true);
    1040            0 :         cockpit.localStorage.clear(false);
    1041              : 
    1042            0 :         if (reload !== false)
    1043            0 :             transport_globals.reload_after_disconnect = true;
    1044            0 :         ensure_transport(function(transport) {
    1045            0 :             if (!transport.send_control({ command: "logout", disconnect: true }))
    1046            0 :                 window.location.reload(transport_globals.reload_after_disconnect);
    1047            0 :         });
    1048            0 :         window.sessionStorage.setItem("logout-intent", "explicit");
    1049            0 :         if (reason)
    1050            0 :             window.sessionStorage.setItem("logout-reason", reason);
    1051            0 :     };
    1052              : 
    1053              :     /* Not public API ... yet? */
    1054            0 :     cockpit.drop_privileges = function drop_privileges() {
    1055            0 :         console.warn("cockpit.drop_privileges() is deprecated");
    1056            0 :         ensure_transport(function(transport) {
    1057            0 :             transport.send_control({ command: "logout", disconnect: false });
    1058            0 :         });
    1059            0 :     };
    1060              : 
    1061              :     /* ---------------------------------------------------------------------
    1062              :      * User and system information
    1063              :      */
    1064              : 
    1065          368 :     cockpit.info = { };
    1066          368 :     event_mixin(cockpit.info, { });
    1067              : 
    1068          365 :     transport_globals.init_callback = function(options) {
    1069          365 :         if (options.system) {
    1070          365 :             cockpit.info.ws = options.system;
    1071          365 :             Object.assign(cockpit.info, options.system);
    1072          365 :         }
    1073          365 :         if (options.system)
    1074          365 :             cockpit.info.dispatchEvent("changed");
    1075              : 
    1076          365 :         cockpit.transport.options = options;
    1077          365 :         cockpit.transport.csrf_token = options["csrf-token"];
    1078          365 :         cockpit.transport.host = transport_globals.default_host;
    1079          365 :     };
    1080              : 
    1081          368 :     let the_user = null;
    1082          368 :     cockpit.user = function () {
    1083          368 :             if (!the_user) {
    1084          368 :                 const dbus = cockpit.dbus(null, { bus: "internal" });
    1085          368 :                 return dbus.call("/user", "org.freedesktop.DBus.Properties", "GetAll",
    1086          368 :                           ["cockpit.User"], { type: "s" })
    1087          362 :                     .then(([user]) => {
    1088          362 :                         the_user = {
    1089          362 :                             id: user.Id.v,
    1090          362 :                             gid: user.Gid?.v,
    1091          362 :                             name: user.Name.v,
    1092          362 :                             full_name: user.Full.v,
    1093          362 :                             groups: user.Groups.v,
    1094          362 :                             home: user.Home.v,
    1095          362 :                             shell: user.Shell.v
    1096          362 :                         };
    1097          362 :                         Object.freeze(the_user);
    1098          362 :                         return the_user;
    1099          362 :                     })
    1100          365 :                     .finally(() => dbus.close());
    1101           85 :             } else {
    1102           85 :                 return Promise.resolve(the_user);
    1103           85 :             }
    1104          368 :     };
    1105              : 
    1106              :     /* ------------------------------------------------------------------------
    1107              :      * Override for broken browser behavior
    1108              :      */
    1109              : 
    1110          296 :     document.addEventListener("click", function(ev) {
    1111          296 :         if (ev.target.classList && in_array(ev.target.classList, 'disabled'))
    1112           26 :           ev.stopPropagation();
    1113          296 :     }, true);
    1114              : 
    1115              :     /* ------------------------------------------------------------------------
    1116              :      * Cockpit location
    1117              :      */
    1118              : 
    1119          368 :     let last_loc = null;
    1120              : 
    1121          368 :     Object.defineProperty(cockpit, "location", {
    1122          368 :         enumerable: true,
    1123          371 :         get: function() {
    1124          366 :             if (!last_loc || last_loc.href !== window.location.hash.slice(1))
    1125          371 :                 last_loc = new Location();
    1126          371 :             return last_loc;
    1127          371 :         },
    1128            0 :         set: function(v) {
    1129            0 :             cockpit.location.go(v);
    1130            0 :         }
    1131          368 :     });
    1132              : 
    1133          162 :     window.addEventListener("hashchange", function() {
    1134          162 :         if (last_loc)
    1135          161 :             last_loc.invalidate();
    1136          162 :         last_loc = null;
    1137          162 :         const hash = window.location.hash.slice(1);
    1138          162 :         cockpit.hint("location", { hash });
    1139          162 :         cockpit.dispatchEvent("locationchanged");
    1140          162 :     });
    1141              : 
    1142              :     /* ------------------------------------------------------------------------
    1143              :      * Cockpit jump
    1144              :      */
    1145              : 
    1146           23 :     cockpit.jump = function jump(path, host) {
    1147           23 :         if (Array.isArray(path))
    1148            0 :             path = "/" + path.map(encodeURIComponent).join("/")
    1149            0 : .replaceAll("%40", "@")
    1150            0 : .replaceAll("%3D", "=")
    1151            0 : .replaceAll("%2B", "+");
    1152              :         else
    1153           23 :             path = "" + path;
    1154              : 
    1155              :         /* When host is not given (undefined), use current transport's host. If
    1156              :          * it is null, use localhost.
    1157              :          */
    1158           23 :         if (host === undefined)
    1159           18 :             host = cockpit.transport.host;
    1160              : 
    1161           23 :         const options = { command: "jump", location: path, host };
    1162           23 :         cockpit.transport.inject("\n" + JSON.stringify(options));
    1163           23 :     };
    1164              : 
    1165              :     /* ---------------------------------------------------------------------
    1166              :      * Cockpit Page Visibility
    1167              :      */
    1168              : 
    1169          368 :     (function() {
    1170          368 :         let hiddenHint = false;
    1171              : 
    1172          399 :         function visibility_change() {
    1173          399 :             let value = document.hidden;
    1174          399 :             if (value === false)
    1175          399 :                 value = hiddenHint;
    1176          399 :             if (cockpit.hidden !== value) {
    1177          399 :                 cockpit.hidden = value;
    1178          399 :                 cockpit.dispatchEvent("visibilitychange");
    1179          399 :             }
    1180          399 :         }
    1181              : 
    1182          368 :         document.addEventListener("visibilitychange", visibility_change);
    1183              : 
    1184              :         /*
    1185              :          * Wait for changes in visibility of just our iframe. These are delivered
    1186              :          * via a hint message from the parent. For now we are the only handler of
    1187              :          * hint messages, so this is implemented rather simply on purpose.
    1188              :          */
    1189          357 :         transport_globals.process_hints = function(data) {
    1190          357 :             if ("hidden" in data) {
    1191          357 :                 hiddenHint = data.hidden;
    1192          357 :                 visibility_change();
    1193          357 :             }
    1194          357 :         };
    1195              : 
    1196              :         /* The first time */
    1197          368 :         visibility_change();
    1198          368 :     }());
    1199              : 
    1200              :     /* ---------------------------------------------------------------------
    1201              :      * Spawning
    1202              :      */
    1203              : 
    1204          272 :     function ProcessError(options, name) {
    1205          131 :         this.problem = options.problem || null;
    1206          272 :         this.exit_status = options["exit-status"];
    1207          272 :         if (this.exit_status === undefined)
    1208          205 :             this.exit_status = null;
    1209          272 :         this.exit_signal = options["exit-signal"];
    1210          272 :         if (this.exit_signal === undefined)
    1211          272 :             this.exit_signal = null;
    1212          272 :         this.message = options.message;
    1213              : 
    1214          205 :         if (this.message === undefined) {
    1215          205 :             if (this.problem)
    1216           39 :                 this.message = cockpit.message(options.problem);
    1217           39 :             else if (this.exit_signal !== null)
    1218           39 :                 this.message = cockpit.format(_("$0 killed with signal $1"), name, this.exit_signal);
    1219           39 :             else if (this.exit_status !== null)
    1220           39 :                 this.message = cockpit.format(_("$0 exited with code $1"), name, this.exit_status);
    1221              :             else
    1222           39 :                 this.message = cockpit.format(_("$0 failed"), name);
    1223           64 :         } else {
    1224          131 :             this.message = this.message.trim();
    1225          131 :         }
    1226              : 
    1227           11 :         this.toString = function() {
    1228           11 :             return this.message;
    1229           11 :         };
    1230          272 :     }
    1231              : 
    1232          368 :     cockpit.ProcessError = ProcessError;
    1233              : 
    1234          360 :     function spawn_debug() {
    1235           67 :         if (window.debugging == "all" || window.debugging?.includes("spawn"))
    1236           64 :             console.debug.apply(console, arguments);
    1237          360 :     }
    1238              : 
    1239              :     /* public */
    1240          360 :     cockpit.spawn = function(command, options) {
    1241          360 :         const dfd = cockpit.defer();
    1242              : 
    1243          360 :         const args = { payload: "stream", spawn: [] };
    1244          360 :         if (command instanceof Array) {
    1245          360 :             for (let i = 0; i < command.length; i++)
    1246          360 :                 args.spawn.push(String(command[i]));
    1247           66 :         } else {
    1248           66 :             args.spawn.push(String(command));
    1249           66 :         }
    1250          360 :         if (options !== undefined)
    1251          337 :             Object.assign(args, options);
    1252              : 
    1253          360 :         spawn_debug("process spawn:", JSON.stringify(args.spawn));
    1254              : 
    1255           64 :         const name = args.spawn[0] || "process";
    1256          360 :         const channel = cockpit.channel(args);
    1257              : 
    1258              :         /* Callback that wants a stream response, see below */
    1259          360 :         const buffer = channel.buffer(null);
    1260              : 
    1261          355 :         channel.addEventListener("close", function(event, options) {
    1262          355 :             const data = buffer.squash();
    1263          355 :             spawn_debug("process closed:", JSON.stringify(options));
    1264          355 :             if (data)
    1265          353 :                 spawn_debug("process output:", data);
    1266          355 :             if (options.message !== undefined)
    1267          355 :                 spawn_debug("process error:", options.message);
    1268              : 
    1269          355 :             if (options.problem)
    1270          217 :                 dfd.reject(new ProcessError(options, name));
    1271          355 :             else if (options["exit-status"] || options["exit-signal"])
    1272          154 :                 dfd.reject(new ProcessError(options, name), data);
    1273          355 :             else if (options.message !== undefined)
    1274           67 :                 dfd.resolve(data, options.message);
    1275              :             else
    1276           67 :                 dfd.resolve(data);
    1277          355 :         });
    1278              : 
    1279          360 :         const ret = dfd.promise;
    1280          198 :         ret.stream = function(callback) {
    1281          198 :             buffer.callback = callback.bind(ret);
    1282          198 :             return this;
    1283          198 :         };
    1284              : 
    1285           20 :         ret.input = function(message, stream) {
    1286           20 :             if (message !== null && message !== undefined) {
    1287           20 :                 spawn_debug("process input:", message);
    1288           20 :                 iterate_data(message, function(data) {
    1289           20 :                     channel.send(data);
    1290           20 :                 });
    1291           20 :             }
    1292           20 :             if (!stream)
    1293           16 :                 channel.control({ command: "done" });
    1294           20 :             return this;
    1295           20 :         };
    1296              : 
    1297          140 :         ret.close = function(problem) {
    1298          140 :             spawn_debug("process closing:", problem);
    1299          140 :             if (channel.valid)
    1300          140 :                 channel.close(problem);
    1301          140 :             return this;
    1302          140 :         };
    1303              : 
    1304          360 :         return ret;
    1305          360 :     };
    1306              : 
    1307              :     /* public */
    1308          234 :     cockpit.script = function(script, args, options) {
    1309          141 :         if (!options && is_plain_object(args)) {
    1310          141 :             options = args;
    1311          141 :             args = [];
    1312          141 :         }
    1313          234 :         const command = ["/bin/sh", "-c", script, "--"];
    1314          234 :         command.push.apply(command, args);
    1315          234 :         return cockpit.spawn(command, options);
    1316          234 :     };
    1317              : 
    1318          368 :     function dbus_debug() {
    1319           70 :         if (window.debugging == "all" || window.debugging?.includes("dbus"))
    1320           67 :             console.debug.apply(console, arguments);
    1321          368 :     }
    1322              : 
    1323          360 :     function DBusError(arg, arg1) {
    1324          145 :         if (typeof (arg) == "string") {
    1325          145 :             this.problem = arg;
    1326          145 :             this.name = null;
    1327          145 :             this.message = arg1 || cockpit.message(arg);
    1328          126 :         } else {
    1329          341 :             this.problem = null;
    1330          341 :             this.name = arg[0];
    1331           65 :             this.message = arg[1][0] || arg[0];
    1332          341 :         }
    1333           15 :         this.toString = function() {
    1334           15 :             return this.message;
    1335           15 :         };
    1336          360 :     }
    1337              : 
    1338          366 :     function DBusCache() {
    1339          366 :         const self = this;
    1340              : 
    1341          366 :         let callbacks = [];
    1342          366 :         self.data = { };
    1343          366 :         self.meta = { };
    1344              : 
    1345          366 :         self.connect = function connect(path, iface, callback, first) {
    1346          366 :             const cb = [path, iface, callback];
    1347          366 :             if (first)
    1348          228 :                 callbacks.unshift(cb);
    1349              :             else
    1350          228 :                 callbacks.push(cb);
    1351          366 :             return {
    1352            0 :                 remove: function remove() {
    1353            0 :                     const length = callbacks.length;
    1354            0 :                     for (let i = 0; i < length; i++) {
    1355            0 :                         const cb = callbacks[i];
    1356            0 :                         if (cb[0] === path && cb[1] === iface && cb[2] === callback) {
    1357            0 :                             delete cb[i];
    1358            0 :                             break;
    1359            0 :                         }
    1360            0 :                     }
    1361            0 :                 }
    1362          366 :             };
    1363          366 :         };
    1364              : 
    1365          361 :         function emit(path, iface, props) {
    1366          361 :             const copy = callbacks.slice();
    1367          361 :             const length = copy.length;
    1368          361 :             for (let i = 0; i < length; i++) {
    1369          361 :                 const cb = copy[i];
    1370          361 :                 if ((!cb[0] || cb[0] === path) &&
    1371          361 :                     (!cb[1] || cb[1] === iface)) {
    1372          361 :                     cb[2](props, path);
    1373          361 :                 }
    1374          361 :             }
    1375          361 :         }
    1376              : 
    1377          361 :         self.update = function update(path, iface, props) {
    1378          361 :             if (!self.data[path])
    1379          361 :                 self.data[path] = { };
    1380          361 :             if (!self.data[path][iface])
    1381          276 :                 self.data[path][iface] = props;
    1382              :             else
    1383          276 :                 props = Object.assign(self.data[path][iface], props);
    1384          361 :             emit(path, iface, props);
    1385          361 :         };
    1386              : 
    1387          107 :         self.remove = function remove(path, iface) {
    1388          107 :             if (self.data[path]) {
    1389          107 :                 delete self.data[path][iface];
    1390          107 :                 emit(path, iface, null);
    1391          107 :             }
    1392          107 :         };
    1393              : 
    1394          366 :         self.lookup = function lookup(path, iface) {
    1395          366 :             if (self.data[path])
    1396          227 :                 return self.data[path][iface];
    1397          366 :             return undefined;
    1398          366 :         };
    1399              : 
    1400          202 :         self.each = function each(iface, callback) {
    1401          129 :             for (const path in self.data) {
    1402          129 :                 for (const ifa in self.data[path]) {
    1403          129 :                     if (ifa == iface)
    1404           40 :                         callback(self.data[path][iface], path);
    1405          129 :                 }
    1406          129 :             }
    1407          202 :         };
    1408              : 
    1409            0 :         self.close = function close() {
    1410            0 :             self.data = { };
    1411            0 :             const copy = callbacks;
    1412            0 :             callbacks = [];
    1413            0 :             const length = copy.length;
    1414            0 :             for (let i = 0; i < length; i++)
    1415            0 :                 copy[i].callback();
    1416            0 :         };
    1417          366 :     }
    1418              : 
    1419          366 :     function DBusProxy(client, cache, iface, path, options) {
    1420          366 :         const self = this;
    1421          366 :         event_mixin(self, { });
    1422              : 
    1423          366 :         let valid = false;
    1424          366 :         let defined = false;
    1425          366 :         const waits = cockpit.defer();
    1426              : 
    1427              :         /* No enumeration on these properties */
    1428          366 :         Object.defineProperties(self, {
    1429          366 :             client: { value: client, enumerable: false, writable: false },
    1430          366 :             path: { value: path, enumerable: false, writable: false },
    1431          366 :             iface: { value: iface, enumerable: false, writable: false },
    1432          363 :             valid: { get: function() { return valid }, enumerable: false },
    1433          366 :             wait: {
    1434          366 :                 enumerable: false,
    1435          366 :                 writable: false,
    1436          363 :                 value: function(func) {
    1437          363 :                     if (func)
    1438          363 :                         waits.promise.always(func);
    1439          363 :                     return waits.promise;
    1440          363 :                 }
    1441          366 :             },
    1442          366 :             call: {
    1443            8 :                 value: function(name, args, options) { return client.call(path, iface, name, args, options) },
    1444          366 :                 enumerable: false,
    1445          366 :                 writable: false
    1446          366 :             },
    1447          366 :             data: { value: { }, enumerable: false }
    1448          366 :         });
    1449              : 
    1450          366 :         if (!options)
    1451           66 :             options = { };
    1452              : 
    1453          361 :         function define() {
    1454          361 :             if (!cache.meta[iface])
    1455          361 :                 return;
    1456              : 
    1457          361 :             const meta = cache.meta[iface];
    1458          361 :             defined = true;
    1459              : 
    1460           66 :             Object.keys(meta.methods || { }).forEach(function(name) {
    1461          361 :                 if (name[0].toLowerCase() == name[0])
    1462          361 :                     return; /* Only map upper case */
    1463              : 
    1464              :                 /* Again, make sure these don't show up in enumerations */
    1465          361 :                 Object.defineProperty(self, name, {
    1466          361 :                     enumerable: false,
    1467          255 :                     value: function() {
    1468          255 :                         const dfd = cockpit.defer();
    1469          255 :                         client.call(path, iface, name, Array.prototype.slice.call(arguments))
    1470          254 :                             .done(function(reply) { dfd.resolve.apply(dfd, reply) })
    1471          133 :                             .fail(function(ex) { dfd.reject(ex) });
    1472          255 :                         return dfd.promise;
    1473          255 :                     }
    1474          361 :                 });
    1475          361 :             });
    1476              : 
    1477           66 :             Object.keys(meta.properties || { }).forEach(function(name) {
    1478          361 :                 if (name[0].toLowerCase() == name[0])
    1479          361 :                     return; /* Only map upper case */
    1480              : 
    1481          361 :                 const config = {
    1482          361 :                     enumerable: true,
    1483          361 :                     get: function() { return self.data[name] },
    1484            0 :                     set: function(v) { throw Error(name + "is not writable") }
    1485          361 :                 };
    1486              : 
    1487          361 :                 const prop = meta.properties[name];
    1488           66 :                 if (prop.flags && prop.flags.indexOf('w') !== -1) {
    1489            0 :                     config.set = function(v) {
    1490            0 :                         client.call(path, "org.freedesktop.DBus.Properties", "Set",
    1491            0 :                                 [iface, name, cockpit.variant(prop.type, v)])
    1492            0 :                             .fail(function(ex) {
    1493            0 :                                 console.log("Couldn't set " + iface + " " + name +
    1494            0 :                                             " at " + path + ": " + ex);
    1495            0 :                             });
    1496            0 :                     };
    1497           66 :                 }
    1498              : 
    1499              :                 /* Again, make sure these don't show up in enumerations */
    1500          361 :                 Object.defineProperty(self, name, config);
    1501          361 :             });
    1502          361 :         }
    1503              : 
    1504          366 :         function update(props) {
    1505          361 :             if (props) {
    1506          361 :                 Object.assign(self.data, props);
    1507          361 :                 if (!defined)
    1508          361 :                     define();
    1509          361 :                 valid = true;
    1510          361 :             } else {
    1511          366 :                 valid = false;
    1512          366 :             }
    1513          366 :             self.dispatchEvent("changed", props);
    1514          366 :         }
    1515              : 
    1516          366 :         cache.connect(path, iface, update, true);
    1517          366 :         update(cache.lookup(path, iface));
    1518              : 
    1519          238 :         function signal(path, iface, name, args) {
    1520          238 :             self.dispatchEvent("signal", name, args);
    1521          238 :             if (name[0].toLowerCase() != name[0]) {
    1522          238 :                 args = args.slice();
    1523          238 :                 args.unshift(name);
    1524          238 :                 self.dispatchEvent.apply(self, args);
    1525          238 :             }
    1526          238 :         }
    1527              : 
    1528          366 :         client.subscribe({ path, interface: iface }, signal, options.subscribe !== false);
    1529              : 
    1530          364 :         function waited(ex) {
    1531              :             // The client.watch call below will be successful for
    1532              :             // non-existing interfaces, but "valid" will be false in
    1533              :             // that case (as it should be). When that happens, our
    1534              :             // argument is not an Error object. So we create the
    1535              :             // "not-found" DBusError ourselves.
    1536              : 
    1537          364 :             if (valid)
    1538          139 :                 waits.resolve();
    1539          142 :             else if (ex instanceof DBusError)
    1540           72 :                 waits.reject(ex);
    1541              :             else
    1542          127 :                 waits.reject(new DBusError("not-found"));
    1543          364 :         }
    1544              : 
    1545              :         /* If watching then do a proper watch, otherwise object is done */
    1546          366 :         if (options.watch !== false)
    1547          215 :             client.watch({ path, interface: iface }).always(waited);
    1548              :         else
    1549          215 :             waited();
    1550          366 :     }
    1551              : 
    1552          202 :     function DBusProxies(client, cache, iface, path_namespace, options) {
    1553          202 :         const self = this;
    1554          202 :         event_mixin(self, { });
    1555              : 
    1556          202 :         self.client = client;
    1557          202 :         self.iface = iface;
    1558          202 :         self.path_namespace = path_namespace;
    1559              : 
    1560          202 :         let waits;
    1561              : 
    1562           92 :         self.wait = function(func) {
    1563           92 :             if (func)
    1564           17 :                 waits.always(func);
    1565           92 :             return waits;
    1566           92 :         };
    1567              : 
    1568          202 :         Object.defineProperties(self, {
    1569          202 :             client: { enumerable: false, writable: false },
    1570          202 :             iface: { enumerable: false, writable: false },
    1571          202 :             path_namespace: { enumerable: false, writable: false },
    1572          202 :             wait: { enumerable: false, writable: false },
    1573          202 :         });
    1574              : 
    1575              :         /* Subscribe to signals once for all proxies */
    1576          202 :         const match = { interface: iface, path_namespace };
    1577              : 
    1578              :         /* Callbacks added by proxies */
    1579          202 :         client.subscribe(match);
    1580              : 
    1581              :         /* Watch for property changes */
    1582          115 :         if (options.watch !== false) {
    1583          115 :             waits = client.watch(match);
    1584           42 :         } else {
    1585          129 :             waits = cockpit.defer().resolve().promise;
    1586          129 :         }
    1587              : 
    1588              :         /* Already added watch/subscribe, tell proxies not to */
    1589          202 :         options = { watch: false, subscribe: false, ...options };
    1590              : 
    1591          187 :         function update(props, path) {
    1592          187 :             let proxy = self[path];
    1593          187 :             if (path) {
    1594          101 :                 if (!props && proxy) {
    1595          101 :                     delete self[path];
    1596          101 :                     self.dispatchEvent("removed", proxy);
    1597          101 :                 } else if (props) {
    1598          187 :                     if (!proxy) {
    1599          187 :                         proxy = self[path] = client.proxy(iface, path, options);
    1600          187 :                         self.dispatchEvent("added", proxy);
    1601          187 :                     }
    1602          187 :                     self.dispatchEvent("changed", proxy);
    1603          187 :                 }
    1604          187 :             }
    1605          187 :         }
    1606              : 
    1607          202 :         cache.connect(null, iface, update, false);
    1608          202 :         cache.each(iface, update);
    1609          202 :     }
    1610              : 
    1611          368 :     function DBusClient(name, options) {
    1612          368 :         const self = this;
    1613          368 :         event_mixin(self, { });
    1614              : 
    1615          368 :         const args = { };
    1616          368 :         let track = false;
    1617          368 :         let owner = null;
    1618              : 
    1619          368 :         if (options) {
    1620          368 :             if (options.track)
    1621          206 :                 track = true;
    1622              : 
    1623          368 :             delete options.track;
    1624          368 :             Object.assign(args, options);
    1625          368 :         }
    1626          368 :         args.payload = "dbus-json3";
    1627          368 :         if (name)
    1628          364 :             args.name = name;
    1629          368 :         self.options = options;
    1630          368 :         self.unique_name = null;
    1631              : 
    1632          368 :         dbus_debug("dbus open: ", args);
    1633              : 
    1634          368 :         let channel = cockpit.channel(args);
    1635          368 :         const subscribers = { };
    1636          368 :         let calls = { };
    1637          368 :         let cache;
    1638              : 
    1639              :         /* The problem we closed with */
    1640          368 :         let closed;
    1641              : 
    1642          368 :         self.constructors = { "*": DBusProxy };
    1643              : 
    1644              :         /* Allows waiting on the channel if necessary */
    1645          368 :         self.wait = channel.wait;
    1646              : 
    1647          366 :         function ensure_cache() {
    1648          366 :             if (!cache)
    1649          366 :                 cache = new DBusCache();
    1650          366 :         }
    1651              : 
    1652          368 :         function send(payload) {
    1653          368 :             if (channel?.valid) {
    1654          368 :                 dbus_debug("dbus:", payload);
    1655          368 :                 channel.send(payload);
    1656          368 :                 return true;
    1657          368 :             }
    1658           79 :             return false;
    1659          368 :         }
    1660              : 
    1661          290 :         function matches(signal, match) {
    1662          281 :             if (match.path && signal[0] !== match.path)
    1663          255 :                 return false;
    1664           41 :             if (match.path_namespace && signal[0].indexOf(match.path_namespace) !== 0)
    1665           41 :                 return false;
    1666          287 :             if (match.interface && signal[1] !== match.interface)
    1667           98 :                 return false;
    1668          131 :             if (match.member && signal[2] !== match.member)
    1669          109 :                 return false;
    1670           41 :             if (match.arg0 && (!signal[3] || signal[3][0] !== match.arg0))
    1671           41 :                 return false;
    1672          290 :             return true;
    1673          290 :         }
    1674              : 
    1675          364 :         function on_message(event, payload) {
    1676          364 :             dbus_debug("dbus:", payload);
    1677          364 :             let msg;
    1678          364 :             try {
    1679          364 :                 msg = JSON.parse(payload);
    1680           67 :             } catch (ex) {
    1681           67 :                 console.warn("received invalid dbus json message:", ex);
    1682           67 :             }
    1683           67 :             if (msg === undefined) {
    1684           67 :                 channel.close({ problem: "protocol-error" });
    1685           67 :                 return;
    1686           67 :             }
    1687          362 :             const dfd = (msg.id !== undefined) ? calls[msg.id] : undefined;
    1688          363 :             if (msg.reply) {
    1689          363 :                 if (dfd) {
    1690          363 :                     const options = { };
    1691          363 :                     if (msg.type)
    1692          362 :                         options.type = msg.type;
    1693          363 :                     if (msg.flags)
    1694          111 :                         options.flags = msg.flags;
    1695          362 :                     dfd.resolve(msg.reply[0] || [], options);
    1696          363 :                     delete calls[msg.id];
    1697          363 :                 }
    1698          363 :                 return;
    1699          343 :             } else if (msg.error) {
    1700          343 :                 if (dfd) {
    1701          343 :                     dfd.reject(new DBusError(msg.error));
    1702          343 :                     delete calls[msg.id];
    1703          343 :                 }
    1704          343 :                 return;
    1705          343 :             }
    1706              : 
    1707              :             /*
    1708              :              * The above promise resolutions or failures are triggered via
    1709              :              * later_invoke(). In order to preserve ordering guarantees we
    1710              :              * also have to process other events that way too.
    1711              :              */
    1712          362 :             later_invoke(function() {
    1713          302 :                 if (msg.signal) {
    1714          302 :                     for (const id in subscribers) {
    1715          302 :                         const subscription = subscribers[id];
    1716          302 :                         if (subscription.callback) {
    1717          302 :                             if (matches(msg.signal, subscription.match))
    1718          302 :                                 subscription.callback.apply(self, msg.signal);
    1719          302 :                         }
    1720          302 :                     }
    1721          302 :                 } else if (msg.notify) {
    1722          361 :                     notify(msg.notify);
    1723          361 :                 } else if (msg.meta) {
    1724          361 :                     meta(msg.meta);
    1725          361 :                 } else if (msg.owner !== undefined) {
    1726          362 :                     self.dispatchEvent("owner", msg.owner);
    1727              : 
    1728              :                     /*
    1729              :                      * We won't get this signal with the same
    1730              :                      * owner twice so if we've seen an owner
    1731              :                      * before that means it has changed.
    1732              :                      */
    1733          205 :                     if (track && owner)
    1734           68 :                         self.close();
    1735              : 
    1736          362 :                     owner = msg.owner;
    1737           66 :                 } else {
    1738           66 :                     dbus_debug("received unexpected dbus json message:", payload);
    1739           66 :                 }
    1740          362 :             });
    1741          364 :         }
    1742              : 
    1743          361 :         function meta(data) {
    1744          361 :             ensure_cache();
    1745          361 :             Object.assign(cache.meta, data);
    1746          361 :             self.dispatchEvent("meta", data);
    1747          361 :         }
    1748              : 
    1749          361 :         function notify(data) {
    1750          361 :             ensure_cache();
    1751          361 :             for (const path in data) {
    1752          361 :                 for (const iface in data[path]) {
    1753          361 :                     const props = data[path][iface];
    1754          361 :                     if (!props)
    1755          157 :                         cache.remove(path, iface);
    1756              :                     else
    1757          361 :                         cache.update(path, iface, props);
    1758          361 :                 }
    1759          361 :             }
    1760          361 :             self.dispatchEvent("notify", data);
    1761          361 :         }
    1762              : 
    1763          368 :         this.notify = notify;
    1764              : 
    1765          161 :         function close_perform(options) {
    1766          144 :             closed = options.problem || "disconnected";
    1767          161 :             const outstanding = calls;
    1768          161 :             calls = { };
    1769           52 :             for (const id in outstanding) {
    1770           52 :                 outstanding[id].reject(new DBusError(closed, options.message));
    1771           52 :             }
    1772          161 :             self.dispatchEvent("close", options);
    1773          161 :         }
    1774              : 
    1775          142 :         this.close = function close(options) {
    1776          142 :             if (typeof options == "string")
    1777           21 :                 options = { problem: options };
    1778          142 :             if (!options)
    1779          142 :                 options = { };
    1780          142 :             if (channel)
    1781           41 :                 channel.close(options);
    1782              :             else
    1783           42 :                 close_perform(options);
    1784          142 :         };
    1785              : 
    1786          365 :         function on_ready(event, message) {
    1787          365 :             dbus_debug("dbus ready:", options);
    1788          365 :             self.unique_name = message["unique-name"];
    1789          365 :         }
    1790              : 
    1791          161 :         function on_close(event, options) {
    1792          161 :             dbus_debug("dbus close:", options);
    1793          161 :             channel.removeEventListener("ready", on_ready);
    1794          161 :             channel.removeEventListener("message", on_message);
    1795          161 :             channel.removeEventListener("close", on_close);
    1796          161 :             channel = null;
    1797          161 :             close_perform(options);
    1798          161 :         }
    1799              : 
    1800          368 :         channel.addEventListener("ready", on_ready);
    1801          368 :         channel.addEventListener("message", on_message);
    1802          368 :         channel.addEventListener("close", on_close);
    1803              : 
    1804          368 :         let last_cookie = 1;
    1805              : 
    1806          368 :         this.call = function call(path, iface, method, args, options) {
    1807          368 :             const dfd = cockpit.defer();
    1808          368 :             const id = String(last_cookie);
    1809          368 :             last_cookie++;
    1810          368 :             const method_call = {
    1811          368 :                 ...options,
    1812          142 :                 call: [path, iface, method, args || []],
    1813          368 :                 id
    1814          368 :             };
    1815              : 
    1816          368 :             const msg = JSON.stringify(method_call);
    1817          368 :             if (send(msg))
    1818           67 :                 calls[id] = dfd;
    1819              :             else
    1820           67 :                 dfd.reject(new DBusError(closed));
    1821              : 
    1822          368 :             return dfd.promise;
    1823          368 :         };
    1824              : 
    1825            0 :         self.signal = function signal(path, iface, member, args, options) {
    1826            0 :             if (!channel || !channel.valid)
    1827            0 :                 return;
    1828              : 
    1829            0 :             const message = { ...options, signal: [path, iface, member, args || []] };
    1830              : 
    1831            0 :             send(JSON.stringify(message));
    1832            0 :         };
    1833              : 
    1834          366 :         this.subscribe = function subscribe(match, callback, rule) {
    1835          366 :             const subscription = {
    1836          366 :                 match: { ...match },
    1837          366 :                 callback
    1838          366 :             };
    1839              : 
    1840          366 :             if (rule !== false)
    1841          366 :                 send(JSON.stringify({ "add-match": subscription.match }));
    1842              : 
    1843          366 :             let id;
    1844          366 :             if (callback) {
    1845          366 :                 id = String(last_cookie);
    1846          366 :                 last_cookie++;
    1847          366 :                 subscribers[id] = subscription;
    1848          366 :             }
    1849              : 
    1850          366 :             return {
    1851           51 :                 remove: function() {
    1852           51 :                     let prev;
    1853           51 :                     if (id) {
    1854           51 :                         prev = subscribers[id];
    1855           51 :                         if (prev)
    1856           51 :                             delete subscribers[id];
    1857           51 :                     }
    1858           51 :                     if (rule !== false && prev)
    1859           51 :                         send(JSON.stringify({ "remove-match": prev.match }));
    1860           51 :                 }
    1861          366 :             };
    1862          366 :         };
    1863              : 
    1864          366 :         self.watch = function watch(path) {
    1865          149 :             const match = is_plain_object(path) ? { ...path } : { path: String(path) };
    1866              : 
    1867          366 :             const id = String(last_cookie);
    1868          366 :             last_cookie++;
    1869          366 :             const dfd = cockpit.defer();
    1870              : 
    1871          366 :             const msg = JSON.stringify({ watch: match, id });
    1872          366 :             if (send(msg))
    1873           78 :                 calls[id] = dfd;
    1874              :             else
    1875           78 :                 dfd.reject(new DBusError(closed));
    1876              : 
    1877          366 :             const ret = dfd.promise;
    1878           20 :             ret.remove = function remove() {
    1879            3 :                 if (id in calls) {
    1880            3 :                     dfd.reject(new DBusError("cancelled"));
    1881            3 :                     delete calls[id];
    1882            3 :                 }
    1883           20 :                 send(JSON.stringify({ unwatch: match }));
    1884           20 :             };
    1885          366 :             return ret;
    1886          366 :         };
    1887              : 
    1888          366 :         self.proxy = function proxy(iface, path, options) {
    1889          366 :             if (!iface)
    1890          360 :                 iface = name;
    1891          366 :             iface = String(iface);
    1892          366 :             if (!path)
    1893          360 :                 path = "/" + iface.replaceAll(".", "/");
    1894          366 :             let Constructor = self.constructors[iface];
    1895          366 :             if (!Constructor)
    1896          366 :                 Constructor = self.constructors["*"];
    1897          366 :             if (!options)
    1898          366 :                 options = { };
    1899          366 :             ensure_cache();
    1900          366 :             return new Constructor(self, cache, iface, String(path), options);
    1901          366 :         };
    1902              : 
    1903          202 :         self.proxies = function proxies(iface, path_namespace, options) {
    1904          202 :             if (!iface)
    1905           40 :                 iface = name;
    1906          202 :             if (!path_namespace)
    1907           40 :                 path_namespace = "/";
    1908          202 :             if (!options)
    1909          115 :                 options = { };
    1910          202 :             ensure_cache();
    1911          202 :             return new DBusProxies(self, cache, String(iface), String(path_namespace), options);
    1912          202 :         };
    1913          368 :     }
    1914              : 
    1915              :     /* Well known buses */
    1916          368 :     const shared_dbus = {
    1917          368 :         internal: null,
    1918          368 :         session: null,
    1919          368 :         system: null,
    1920          368 :     };
    1921              : 
    1922              :     /* public */
    1923          368 :     cockpit.dbus = function dbus(name, options) {
    1924          368 :         if (!options)
    1925          318 :             options = { bus: "system" };
    1926              : 
    1927              :         /*
    1928              :          * Figure out if this we should use a shared bus.
    1929              :          *
    1930              :          * This is only the case if a null name *and* the
    1931              :          * options are just a simple { "bus": "xxxx" }
    1932              :          */
    1933          368 :         const keys = Object.keys(options);
    1934          368 :         const bus = options.bus;
    1935          368 :         const shared = !name && keys.length == 1 && bus in shared_dbus;
    1936              : 
    1937          368 :         if (shared && shared_dbus[bus])
    1938          367 :             return shared_dbus[bus];
    1939              : 
    1940          368 :         const client = new DBusClient(name, options);
    1941              : 
    1942              :         /*
    1943              :          * Store the shared bus for next time. Override the
    1944              :          * close function to only work when a problem is
    1945              :          * indicated.
    1946              :          */
    1947          368 :         if (shared) {
    1948          368 :             const old_close = client.close;
    1949          365 :             client.close = function() {
    1950          365 :                 if (arguments.length > 0)
    1951           67 :                     old_close.apply(client, arguments);
    1952          365 :             };
    1953           18 :             client.addEventListener("close", function() {
    1954           18 :                 if (shared_dbus[bus] == client)
    1955           18 :                     shared_dbus[bus] = null;
    1956           18 :             });
    1957          368 :             shared_dbus[bus] = client;
    1958          368 :         }
    1959              : 
    1960          368 :         return client;
    1961          368 :     };
    1962              : 
    1963           42 :     cockpit.variant = function variant(type, value) {
    1964           42 :         return { v: value, t: type };
    1965           42 :     };
    1966              : 
    1967            0 :     cockpit.byte_array = function byte_array(string) {
    1968            0 :         console.warn("cockpit.byte_array() is deprecated, use window.btoa");
    1969            0 :         return window.btoa(string);
    1970            0 :     };
    1971              : 
    1972              :     /* File access
    1973              :      */
    1974              : 
    1975          364 :     cockpit.file = function file(path, options) {
    1976          193 :         options = options || { };
    1977          364 :         const binary = options.binary;
    1978              : 
    1979          364 :         const self = {
    1980          364 :             path,
    1981          364 :             read,
    1982          364 :             replace,
    1983          364 :             modify,
    1984              : 
    1985          364 :             watch,
    1986              : 
    1987          364 :             close
    1988          364 :         };
    1989              : 
    1990          364 :         const base_channel_options = { ...options };
    1991          364 :         delete base_channel_options.syntax;
    1992              : 
    1993          363 :         function parse(str) {
    1994          343 :             if (options.syntax?.parse)
    1995          168 :                 return options.syntax.parse(str);
    1996              :             else
    1997          188 :                 return str;
    1998          363 :         }
    1999              : 
    2000           10 :         function stringify(obj) {
    2001            0 :             if (options.syntax?.stringify)
    2002            0 :                 return options.syntax.stringify(obj);
    2003              :             else
    2004           10 :                 return obj;
    2005           10 :         }
    2006              : 
    2007          364 :         let read_promise = null;
    2008          364 :         let read_channel;
    2009              : 
    2010          364 :         function read() {
    2011          364 :             if (read_promise)
    2012           67 :                 return read_promise;
    2013              : 
    2014          364 :             const dfd = cockpit.defer();
    2015          364 :             const opts = {
    2016          364 :                 ...base_channel_options,
    2017          364 :                 payload: "fsread1",
    2018          364 :                 path
    2019          364 :             };
    2020              : 
    2021          364 :             function try_read() {
    2022          364 :                 read_channel = cockpit.channel(opts);
    2023          364 :                 const content_parts = [];
    2024          363 :                 read_channel.addEventListener("message", function (event, message) {
    2025          363 :                     content_parts.push(message);
    2026          363 :                 });
    2027          364 :                 read_channel.addEventListener("close", function (event, message) {
    2028          364 :                     read_channel = null;
    2029              : 
    2030           67 :                     if (message.problem == "change-conflict") {
    2031           67 :                         try_read();
    2032           67 :                         return;
    2033           67 :                     }
    2034              : 
    2035          364 :                     read_promise = null;
    2036              : 
    2037           67 :                     if (message.problem) {
    2038           67 :                         const error = new BasicError(message.problem, message.message);
    2039           67 :                         fire_watch_callbacks(null, null, error);
    2040           67 :                         dfd.reject(error);
    2041           67 :                         return;
    2042           67 :                     }
    2043              : 
    2044          364 :                     let content;
    2045          364 :                     if (message.tag == "-")
    2046           84 :                         content = null;
    2047          364 :                     else {
    2048          364 :                         try {
    2049          364 :                             content = parse(join_data(content_parts, binary));
    2050           67 :                         } catch (e) {
    2051           67 :                             fire_watch_callbacks(null, null, e);
    2052           67 :                             dfd.reject(e);
    2053           67 :                             return;
    2054           67 :                         }
    2055          364 :                     }
    2056              : 
    2057          364 :                     fire_watch_callbacks(content, message.tag);
    2058          364 :                     dfd.resolve(content, message.tag);
    2059          364 :                 });
    2060          364 :             }
    2061              : 
    2062          364 :             try_read();
    2063              : 
    2064          364 :             read_promise = dfd.promise;
    2065          364 :             return read_promise;
    2066          364 :         }
    2067              : 
    2068          364 :         let replace_channel = null;
    2069              : 
    2070           14 :         function replace(new_content, expected_tag) {
    2071           14 :             const dfd = cockpit.defer();
    2072              : 
    2073           14 :             let file_content;
    2074           14 :             try {
    2075            2 :                 file_content = (new_content === null) ? null : stringify(new_content);
    2076            0 :             } catch (e) {
    2077            0 :                 dfd.reject(e);
    2078            0 :                 return dfd.promise;
    2079            0 :             }
    2080              : 
    2081           14 :             if (replace_channel)
    2082            0 :                 replace_channel.close("abort");
    2083              : 
    2084           14 :             const opts = {
    2085           14 :                 ...base_channel_options,
    2086           14 :                 payload: "fsreplace1",
    2087           14 :                 path,
    2088           14 :                 tag: expected_tag
    2089           14 :             };
    2090           14 :             replace_channel = cockpit.channel(opts);
    2091              : 
    2092           14 :             replace_channel.addEventListener("close", function (event, message) {
    2093           14 :                 replace_channel = null;
    2094            1 :                 if (message.problem) {
    2095            1 :                     dfd.reject(new BasicError(message.problem, message.message));
    2096            1 :                 } else {
    2097           14 :                     fire_watch_callbacks(new_content, message.tag);
    2098           14 :                     dfd.resolve(message.tag);
    2099           14 :                 }
    2100           14 :             });
    2101              : 
    2102              :             // null means 'erase this file', which is what will happen if
    2103              :             // we send no data. the empty string means "write an empty
    2104              :             // file", and in order to do that, we need to explicitly send
    2105              :             // an empty frame (or we'll delete the file). iterate_data()
    2106              :             // doesn't call us if the string is empty, so we handle it.
    2107           10 :             if (file_content !== null) {
    2108            0 :                 if (file_content.length === 0 || file_content.byteLength === 0) {
    2109            0 :                     replace_channel.send(file_content);
    2110            0 :                 } else {
    2111           10 :                     iterate_data(file_content, data => {
    2112           10 :                         replace_channel.send(data);
    2113           10 :                     });
    2114           10 :                 }
    2115           10 :             }
    2116              : 
    2117           14 :             replace_channel.control({ command: "done" });
    2118           14 :             return dfd.promise;
    2119           14 :         }
    2120              : 
    2121            6 :         function modify(callback, initial_content, initial_tag) {
    2122            6 :             const dfd = cockpit.defer();
    2123              : 
    2124            6 :             function update(content, tag) {
    2125            6 :                 let new_content = callback(content);
    2126            6 :                 if (new_content === undefined)
    2127            0 :                     new_content = content;
    2128            6 :                 replace(new_content, tag)
    2129            6 :                     .done(function (new_tag) {
    2130            6 :                         dfd.resolve(new_content, new_tag);
    2131            6 :                     })
    2132            0 :                     .fail(function (error) {
    2133            0 :                         if (error.problem == "change-conflict")
    2134            0 :                             read_then_update();
    2135              :                         else
    2136            0 :                             dfd.reject(error);
    2137            0 :                     });
    2138            6 :             }
    2139              : 
    2140            6 :             function read_then_update() {
    2141            6 :                 read()
    2142            6 :                     .done(update)
    2143            0 :                     .fail(function (error) {
    2144            0 :                         dfd.reject(error);
    2145            0 :                     });
    2146            6 :             }
    2147              : 
    2148            6 :             if (initial_content === undefined)
    2149            0 :                 read_then_update();
    2150              :             else
    2151            0 :                 update(initial_content, initial_tag);
    2152              : 
    2153            6 :             return dfd.promise;
    2154            6 :         }
    2155              : 
    2156          364 :         const watch_callbacks = [];
    2157          364 :         let n_watch_callbacks = 0;
    2158              : 
    2159          364 :         let watch_channel = null;
    2160          364 :         let watch_tag;
    2161              : 
    2162          118 :         function ensure_watch_channel(options) {
    2163          118 :             if (n_watch_callbacks > 0) {
    2164          118 :                 if (watch_channel)
    2165          118 :                     return;
    2166              : 
    2167          118 :                 watch_channel = new FsInfoClient(path, ["tag"], { superuser: base_channel_options.superuser });
    2168          117 :                 watch_channel.on('change', (state) => {
    2169          108 :                     if (state.error) {
    2170              :                         // Behave like fsread1, not-found is not a fatal error
    2171          108 :                         if (state.error.problem === "not-found") {
    2172          108 :                             fire_watch_callbacks(null, "-");
    2173           23 :                         } else {
    2174           23 :                             const error = new BasicError(state.error.problem, state.error.message);
    2175           23 :                             fire_watch_callbacks(null, null, error);
    2176           23 :                         }
    2177          102 :                     } else if (state.info && state.info.tag) {
    2178              :                         // otherwise, the file is present with the given tag
    2179          111 :                         if (state.info.tag !== watch_tag) {
    2180              :                             // cockpit.file.watch() defaults to reading
    2181           34 :                             if (options?.read === false)
    2182           34 :                                 fire_watch_callbacks(null, state.info.tag);
    2183              :                             else
    2184          111 :                                 read();
    2185          111 :                         }
    2186          111 :                     }
    2187          117 :                 });
    2188              : 
    2189              :                 // fallback when running against bridge < 310
    2190           17 :                 watch_channel.on('close', ex => {
    2191            3 :                     if (ex.problem === 'not-supported') {
    2192            3 :                         const opts = {
    2193            3 :                             payload: "fswatch1",
    2194            3 :                             path,
    2195            3 :                             superuser: base_channel_options.superuser,
    2196            3 :                         };
    2197            3 :                         watch_channel = cockpit.channel(opts);
    2198            0 :                         watch_channel.addEventListener("message", (event, message_string) => {
    2199            0 :                             let message;
    2200            0 :                             try {
    2201            0 :                                 message = JSON.parse(message_string);
    2202            0 :                             } catch (e) {
    2203            0 :                                 message = null;
    2204            0 :                             }
    2205            0 :                             if (message && message.path == path && message.tag && message.tag != watch_tag) {
    2206            0 :                                 if (options && options.read !== undefined && !options.read)
    2207            0 :                                     fire_watch_callbacks(null, message.tag);
    2208              :                                 else
    2209            0 :                                     read();
    2210            0 :                             }
    2211            0 :                         });
    2212              :                         // trigger initial watch event
    2213            3 :                         read();
    2214            3 :                     }
    2215           17 :                 });
    2216           25 :             } else {
    2217           25 :                 if (watch_channel) {
    2218           25 :                     watch_channel.close();
    2219           25 :                     watch_channel = null;
    2220           25 :                 }
    2221           25 :             }
    2222          118 :         }
    2223              : 
    2224          364 :         function fire_watch_callbacks(/* content, tag, error */) {
    2225           69 :             watch_tag = arguments[1] || null;
    2226          364 :             invoke_functions(watch_callbacks, self, arguments);
    2227          364 :         }
    2228              : 
    2229          118 :         function watch(callback, options) {
    2230          118 :             if (callback)
    2231          118 :                 watch_callbacks.push(callback);
    2232          118 :             n_watch_callbacks += 1;
    2233          118 :             ensure_watch_channel(options);
    2234              : 
    2235          118 :             watch_tag = null;
    2236              : 
    2237          118 :             return {
    2238            4 :                 remove: function () {
    2239            4 :                     if (callback) {
    2240            4 :                         const index = watch_callbacks.indexOf(callback);
    2241            4 :                         if (index > -1)
    2242            4 :                             watch_callbacks[index] = null;
    2243            4 :                     }
    2244            4 :                     n_watch_callbacks -= 1;
    2245            4 :                     ensure_watch_channel(options);
    2246            4 :                 }
    2247          118 :             };
    2248          118 :         }
    2249              : 
    2250           95 :         function close() {
    2251           95 :             if (read_channel)
    2252           17 :                 read_channel.close("cancelled");
    2253           95 :             if (replace_channel)
    2254           17 :                 replace_channel.close("cancelled");
    2255           95 :             if (watch_channel)
    2256           18 :                 watch_channel.close();
    2257           95 :         }
    2258              : 
    2259          364 :         return self;
    2260          364 :     };
    2261              : 
    2262              :     /* ---------------------------------------------------------------------
    2263              :      * Localization
    2264              :      */
    2265              : 
    2266          368 :     let po_data = { };
    2267          368 :     let po_plural;
    2268              : 
    2269          368 :     cockpit.language = "en";
    2270          368 :     cockpit.language_direction = "ltr";
    2271          368 :     const test_l10n = window.localStorage.test_l10n;
    2272              : 
    2273            3 :     cockpit.locale = function locale(po) {
    2274            3 :         let lang = cockpit.language;
    2275            3 :         let lang_dir = cockpit.language_direction;
    2276            3 :         let header;
    2277              : 
    2278            3 :         if (po) {
    2279            3 :             Object.assign(po_data, po);
    2280            3 :             header = po[""];
    2281            2 :         } else if (po === null) {
    2282            2 :             po_data = { };
    2283            2 :         }
    2284              : 
    2285            3 :         if (header) {
    2286            3 :             if (header["plural-forms"])
    2287            3 :                 po_plural = header["plural-forms"];
    2288            3 :             if (header.language)
    2289            3 :                 lang = header.language;
    2290            3 :             if (header["language-direction"])
    2291            3 :                 lang_dir = header["language-direction"];
    2292            3 :         }
    2293              : 
    2294            3 :         cockpit.language = lang;
    2295            3 :         cockpit.language_direction = lang_dir;
    2296            3 :     };
    2297              : 
    2298          364 :     cockpit.translate = function translate(/* ... */) {
    2299          364 :         let what;
    2300              : 
    2301              :         /* Called without arguments, entire document */
    2302          364 :         if (arguments.length === 0)
    2303           66 :             what = [document];
    2304              : 
    2305              :         /* Called with a single array like argument */
    2306           66 :         else if (arguments.length === 1 && arguments[0].length)
    2307           66 :             what = arguments[0];
    2308              : 
    2309              :         /* Called with 1 or more element arguments */
    2310              :         else
    2311           66 :             what = arguments;
    2312              : 
    2313              :         /* Translate all the things */
    2314          364 :         const wlen = what.length;
    2315          364 :         for (let w = 0; w < wlen; w++) {
    2316              :             /* The list of things to translate */
    2317          364 :             let list = null;
    2318          364 :             if (what[w].querySelectorAll)
    2319          364 :                 list = what[w].querySelectorAll("[translate]");
    2320          364 :             if (!list)
    2321          364 :                 continue;
    2322              : 
    2323              :             /* Each element */
    2324          108 :             for (let i = 0; i < list.length; i++) {
    2325          108 :                 const el = list[i];
    2326              : 
    2327           66 :                 let val = el.getAttribute("translate") || "yes";
    2328          108 :                 if (val == "no")
    2329          108 :                     continue;
    2330              : 
    2331              :                 /* Each thing to translate */
    2332          108 :                 const tasks = val.split(" ");
    2333          108 :                 val = el.getAttribute("translate-context") || el.getAttribute("context");
    2334          108 :                 for (let t = 0; t < tasks.length; t++) {
    2335           66 :                     if (tasks[t] == "yes" || tasks[t] == "translate")
    2336           66 :                         el.textContent = cockpit.gettext(val, el.textContent);
    2337           66 :                     else if (tasks[t])
    2338           66 :                         el.setAttribute(tasks[t], cockpit.gettext(val, el.getAttribute(tasks[t]) || ""));
    2339          108 :                 }
    2340              : 
    2341              :                 /* Mark this thing as translated */
    2342          108 :                 el.removeAttribute("translate");
    2343          108 :             }
    2344          364 :         }
    2345          364 :     };
    2346              : 
    2347          394 :     cockpit.gettext = function gettext(context, string) {
    2348              :         /* Missing first parameter */
    2349          394 :         if (arguments.length == 1) {
    2350          394 :             string = context;
    2351          394 :             context = undefined;
    2352          394 :         }
    2353              : 
    2354          211 :         const key = context ? context + '\u0004' + string : string;
    2355          394 :         if (po_data) {
    2356          394 :             const translated = po_data[key];
    2357           93 :             if (translated?.[1])
    2358           93 :                 string = translated[1];
    2359          394 :         }
    2360              : 
    2361          394 :         if (test_l10n === 'true')
    2362           93 :             return "»" + string + "«";
    2363              : 
    2364          394 :         return string;
    2365          394 :     };
    2366              : 
    2367            2 :     function imply(val) {
    2368            1 :         return (val === true ? 1 : val || 0);
    2369            2 :     }
    2370              : 
    2371          183 :     cockpit.ngettext = function ngettext(context, string1, stringN, num) {
    2372              :         /* Missing first parameter */
    2373          183 :         if (arguments.length == 3) {
    2374          183 :             num = stringN;
    2375          183 :             stringN = string1;
    2376          183 :             string1 = context;
    2377          183 :             context = undefined;
    2378          183 :         }
    2379              : 
    2380           27 :         const key = context ? context + '\u0004' + string1 : string1;
    2381           27 :         if (po_data && po_plural) {
    2382           27 :             const translated = po_data[key];
    2383           27 :             if (translated) {
    2384           27 :                 const i = imply(po_plural(num)) + 1;
    2385           27 :                 if (translated[i])
    2386           27 :                     return translated[i];
    2387           27 :             }
    2388           27 :         }
    2389          183 :         if (num == 1)
    2390          156 :             return string1;
    2391           73 :         return stringN;
    2392          183 :     };
    2393              : 
    2394            0 :     cockpit.noop = function noop(arg0, arg1) {
    2395            0 :         return arguments[arguments.length - 1];
    2396            0 :     };
    2397              : 
    2398              :     /* Only for _() calls here in the cockpit code */
    2399          368 :     const _ = cockpit.gettext;
    2400              : 
    2401          265 :     cockpit.message = function message(arg) {
    2402          265 :         if (arg.message)
    2403           37 :             return arg.message;
    2404              : 
    2405          264 :         let problem = null;
    2406          264 :         if (arg.problem)
    2407           44 :             problem = arg.problem;
    2408              :         else
    2409          262 :             problem = arg + "";
    2410          264 :         if (problem == "terminated")
    2411           35 :             return _("Your session has been terminated.");
    2412          263 :         else if (problem == "no-session")
    2413           34 :             return _("Your session has expired. Please log in again.");
    2414          263 :         else if (problem == "access-denied")
    2415           39 :             return _("Not permitted to perform this action.");
    2416          259 :         else if (problem == "authentication-failed")
    2417           35 :             return _("Login failed");
    2418          259 :         else if (problem == "authentication-not-supported")
    2419           34 :             return _("The server refused to authenticate using any supported methods.");
    2420          259 :         else if (problem == "unknown-hostkey")
    2421           34 :             return _("Untrusted host");
    2422          259 :         else if (problem == "unknown-host")
    2423           34 :             return _("Untrusted host");
    2424          259 :         else if (problem == "invalid-hostkey")
    2425           34 :             return _("Host key is incorrect");
    2426          259 :         else if (problem == "internal-error")
    2427           34 :             return _("Internal error");
    2428          259 :         else if (problem == "timeout")
    2429           34 :             return _("Connection has timed out.");
    2430          259 :         else if (problem == "no-cockpit")
    2431           34 :             return _("Cockpit is not installed on the system.");
    2432          258 :         else if (problem == "no-forwarding")
    2433           34 :             return _("Cannot forward login credentials");
    2434          258 :         else if (problem == "disconnected")
    2435           50 :             return _("Server has closed the connection.");
    2436          254 :         else if (problem == "not-supported")
    2437           34 :             return _("Cockpit is not compatible with the software on the system.");
    2438          254 :         else if (problem == "no-host")
    2439           34 :             return _("Cockpit could not contact the given host.");
    2440          254 :         else if (problem == "too-large")
    2441           34 :             return _("Too much data");
    2442              :         else
    2443          254 :             return problem;
    2444          265 :     };
    2445              : 
    2446            0 :     function HttpError(arg0, arg1, message) {
    2447            0 :         this.status = parseInt(arg0, 10);
    2448            0 :         this.reason = arg1;
    2449            0 :         this.message = message || arg1;
    2450            0 :         this.problem = null;
    2451              : 
    2452            0 :         this.valueOf = function() {
    2453            0 :             return this.status;
    2454            0 :         };
    2455            0 :         this.toString = function() {
    2456            0 :             return this.status + " " + this.message;
    2457            0 :         };
    2458            0 :     }
    2459              : 
    2460            0 :     function http_debug() {
    2461            0 :         if (window.debugging == "all" || window.debugging?.includes("http"))
    2462            0 :             console.debug.apply(console, arguments);
    2463            0 :     }
    2464              : 
    2465            0 :     function find_header(headers, name) {
    2466            0 :         if (!headers)
    2467            0 :             return undefined;
    2468            0 :         name = name.toLowerCase();
    2469            0 :         for (const head in headers) {
    2470            0 :             if (head.toLowerCase() == name)
    2471            0 :                 return headers[head];
    2472            0 :         }
    2473            0 :         return undefined;
    2474            0 :     }
    2475              : 
    2476            0 :     function HttpClient(endpoint, options) {
    2477            0 :         const self = this;
    2478              : 
    2479            0 :         self.options = options;
    2480            0 :         options.payload = "http-stream2";
    2481              : 
    2482            0 :         const active_requests = [];
    2483              : 
    2484            0 :         if (endpoint !== undefined) {
    2485            0 :             if (endpoint.indexOf && endpoint.indexOf("/") === 0) {
    2486            0 :                 options.unix = endpoint;
    2487            0 :             } else {
    2488            0 :                 const port = parseInt(endpoint, 10);
    2489            0 :                 if (!isNaN(port))
    2490            0 :                     options.port = port;
    2491              :                 else
    2492            0 :                     throw Error("The endpoint must be either a unix path or port number");
    2493            0 :             }
    2494            0 :         }
    2495              : 
    2496            0 :         if (options.address) {
    2497            0 :             if (!options.capabilities)
    2498            0 :                 options.capabilities = [];
    2499            0 :             options.capabilities.push("address");
    2500            0 :         }
    2501              : 
    2502            0 :         function param(obj) {
    2503            0 :             return Object.keys(obj).map(function(k) {
    2504            0 :                 return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]);
    2505            0 :             })
    2506            0 : .join('&')
    2507            0 : .split('%20')
    2508            0 : .join('+'); /* split/join because phantomjs */
    2509            0 :         }
    2510              : 
    2511            0 :         self.request = function request(req) {
    2512            0 :             const dfd = cockpit.defer();
    2513            0 :             const ret = dfd.promise;
    2514              : 
    2515            0 :             if (!req.path)
    2516            0 :                 req.path = "/";
    2517            0 :             if (!req.method)
    2518            0 :                 req.method = "GET";
    2519            0 :             if (req.params) {
    2520            0 :                 if (req.path.indexOf("?") === -1)
    2521            0 :                     req.path += "?" + param(req.params);
    2522              :                 else
    2523            0 :                     req.path += "&" + param(req.params);
    2524            0 :             }
    2525            0 :             delete req.params;
    2526              : 
    2527            0 :             const input = req.body;
    2528            0 :             delete req.body;
    2529              : 
    2530            0 :             const headers = req.headers;
    2531            0 :             delete req.headers;
    2532              : 
    2533            0 :             Object.assign(req, options);
    2534              : 
    2535              :             /* Combine the headers */
    2536            0 :             if (options.headers && headers)
    2537            0 :                 req.headers = { ...options.headers, ...headers };
    2538            0 :             else if (options.headers)
    2539            0 :                 req.headers = options.headers;
    2540              :             else
    2541            0 :                 req.headers = headers;
    2542              : 
    2543            0 :             http_debug("http request:", JSON.stringify(req));
    2544              : 
    2545              :             /* We need a channel for the request */
    2546            0 :             const channel = cockpit.channel(req);
    2547              : 
    2548            0 :             if (input !== undefined) {
    2549            0 :                 if (input !== "") {
    2550            0 :                     http_debug("http input:", input);
    2551            0 :                     iterate_data(input, function(data) {
    2552            0 :                         channel.send(data);
    2553            0 :                     });
    2554            0 :                 }
    2555            0 :                 http_debug("http", req.method, req.path, "request sent, channel done");
    2556            0 :                 channel.control({ command: "done" });
    2557            0 :             }
    2558              : 
    2559              :             /* Callbacks that want to stream or get headers */
    2560            0 :             let streamer = null;
    2561            0 :             let responsers = null;
    2562              : 
    2563            0 :             let resp = null;
    2564              : 
    2565            0 :             const buffer = channel.buffer(function(data) {
    2566              :                 /* Fire any streamers */
    2567            0 :                 if (resp && resp.status >= 200 && resp.status <= 299 && streamer)
    2568            0 :                     return streamer.call(ret, data);
    2569            0 :                 return 0;
    2570            0 :             });
    2571              : 
    2572            0 :             function on_control(event, options) {
    2573              :                 /* Anyone looking for response details? */
    2574            0 :                 if (options.command == "response") {
    2575            0 :                     resp = options;
    2576            0 :                     if (responsers) {
    2577            0 :                         resp.headers = resp.headers || { };
    2578            0 :                         invoke_functions(responsers, ret, [resp.status, resp.headers]);
    2579            0 :                     }
    2580            0 :                 }
    2581            0 :             }
    2582              : 
    2583            0 :             function on_close(event, options) {
    2584            0 :                 const pos = active_requests.indexOf(ret);
    2585            0 :                 if (pos >= 0)
    2586            0 :                     active_requests.splice(pos, 1);
    2587              : 
    2588            0 :                 if (options.problem) {
    2589            0 :                     http_debug("http problem: ", options.problem);
    2590            0 :                     dfd.reject(new BasicError(options.problem, options.message));
    2591            0 :                 } else {
    2592            0 :                     const body = buffer.squash();
    2593              : 
    2594              :                     /* An error, fail here */
    2595            0 :                     if (resp && (resp.status < 200 || resp.status > 299)) {
    2596            0 :                         let message;
    2597            0 :                         const type = find_header(resp.headers, "Content-Type");
    2598            0 :                         if (type && !channel.binary) {
    2599            0 :                             if (type.indexOf("text/plain") === 0)
    2600            0 :                                 message = body;
    2601            0 :                         }
    2602            0 :                         http_debug("http", req.method, req.path, "failed:", resp.status, resp.reason);
    2603            0 :                         dfd.reject(new HttpError(resp.status, resp.reason, message), body);
    2604            0 :                     } else {
    2605            0 :                         if (resp)
    2606            0 :                             http_debug("http", req.method, req.path, "succeeded:", resp.status);
    2607              :                         else
    2608            0 :                             http_debug("http", req.method, req.path, "failed without response");
    2609            0 :                         dfd.resolve(body);
    2610            0 :                     }
    2611            0 :                 }
    2612              : 
    2613            0 :                 channel.removeEventListener("control", on_control);
    2614            0 :                 channel.removeEventListener("close", on_close);
    2615            0 :             }
    2616              : 
    2617            0 :             channel.addEventListener("control", on_control);
    2618            0 :             channel.addEventListener("close", on_close);
    2619              : 
    2620            0 :             ret.stream = function(callback) {
    2621            0 :                 streamer = callback;
    2622            0 :                 return ret;
    2623            0 :             };
    2624            0 :             ret.response = function(callback) {
    2625            0 :                 if (responsers === null)
    2626            0 :                     responsers = [];
    2627            0 :                 responsers.push(callback);
    2628            0 :                 return ret;
    2629            0 :             };
    2630            0 :             ret.input = function(message, stream) {
    2631            0 :                 if (message !== null && message !== undefined) {
    2632            0 :                     http_debug("http input:", message);
    2633            0 :                     iterate_data(message, function(data) {
    2634            0 :                         channel.send(data);
    2635            0 :                     });
    2636            0 :                 }
    2637            0 :                 if (!stream) {
    2638            0 :                     http_debug("http done");
    2639            0 :                     channel.control({ command: "done" });
    2640            0 :                 }
    2641            0 :                 return ret;
    2642            0 :             };
    2643            0 :             ret.close = function(problem) {
    2644            0 :                 http_debug("http closing:", problem);
    2645            0 :                 channel.close(problem);
    2646            0 :                 return ret;
    2647            0 :             };
    2648              : 
    2649            0 :             active_requests.push(ret);
    2650            0 :             return ret;
    2651            0 :         };
    2652              : 
    2653            0 :         self.get = function get(path, params, headers) {
    2654            0 :             return self.request({
    2655            0 :                 method: "GET",
    2656            0 :                 params,
    2657            0 :                 path,
    2658            0 :                 body: "",
    2659            0 :                 headers
    2660            0 :             });
    2661            0 :         };
    2662              : 
    2663            0 :         self.post = function post(path, body, headers) {
    2664            0 :             headers = headers || { };
    2665              : 
    2666            0 :             if (is_plain_object(body) || Array.isArray(body)) {
    2667            0 :                 body = JSON.stringify(body);
    2668            0 :                 if (find_header(headers, "Content-Type") === undefined)
    2669            0 :                     headers["Content-Type"] = "application/json";
    2670            0 :             } else if (body === undefined || body === null) {
    2671            0 :                 body = "";
    2672            0 :             } else if (typeof body !== "string") {
    2673            0 :                 body = String(body);
    2674            0 :             }
    2675              : 
    2676            0 :             return self.request({
    2677            0 :                 method: "POST",
    2678            0 :                 path,
    2679            0 :                 body,
    2680            0 :                 headers
    2681            0 :             });
    2682            0 :         };
    2683              : 
    2684            0 :         self.close = function close(problem) {
    2685            0 :             const reqs = active_requests.slice();
    2686            0 :             for (let i = 0; i < reqs.length; i++)
    2687            0 :                 reqs[i].close(problem);
    2688            0 :         };
    2689            0 :     }
    2690              : 
    2691              :     /* public */
    2692            0 :     cockpit.http = function(endpoint, options) {
    2693            0 :         if (is_plain_object(endpoint) && options === undefined) {
    2694            0 :             options = endpoint;
    2695            0 :             endpoint = undefined;
    2696            0 :         }
    2697            0 :         return new HttpClient(endpoint, options || { });
    2698            0 :     };
    2699              : 
    2700              :     /* ---------------------------------------------------------------------
    2701              :      * Permission
    2702              :      */
    2703              : 
    2704            2 :     function check_superuser() {
    2705            2 :         return new Promise((resolve, reject) => {
    2706            2 :             const ch = cockpit.channel({ payload: "null", superuser: "require" });
    2707            2 :             ch.wait()
    2708            1 :                 .then(() => resolve(true))
    2709            1 :                 .catch(() => resolve(false))
    2710            2 :                 .always(() => ch.close());
    2711            2 :         });
    2712            2 :     }
    2713              : 
    2714            2 :     function Permission(options) {
    2715            2 :         const self = this;
    2716            2 :         event_mixin(self, { });
    2717              : 
    2718            2 :         const api = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
    2719            2 :         api.addEventListener("changed", maybe_reload);
    2720              : 
    2721            2 :         function maybe_reload() {
    2722            2 :             if (api.valid && self.allowed !== null) {
    2723            2 :                 if (self.allowed != (api.Current != "none"))
    2724            2 :                     window.location.reload(true);
    2725            2 :             }
    2726            2 :         }
    2727              : 
    2728            2 :         self.allowed = null;
    2729            1 :         self.user = options ? options.user : null; // pre-fill for unit tests
    2730            1 :         self.is_superuser = options ? options._is_superuser : null; // pre-fill for unit tests
    2731              : 
    2732            2 :         let group = null;
    2733            2 :         let admin = false;
    2734              : 
    2735            2 :         if (options)
    2736            2 :             group = options.group;
    2737              : 
    2738            2 :         if (options?.admin)
    2739            2 :             admin = true;
    2740              : 
    2741            2 :         function decide(user) {
    2742            2 :             if (user.id === 0)
    2743            1 :                 return true;
    2744              : 
    2745            2 :             if (group)
    2746            1 :                 return !!(user.groups || []).includes(group);
    2747              : 
    2748            2 :             if (admin)
    2749            2 :                 return self.is_superuser;
    2750              : 
    2751            1 :             if (user.id === undefined)
    2752            1 :                 return null;
    2753              : 
    2754            1 :             return false;
    2755            2 :         }
    2756              : 
    2757            1 :         if (self.user && self.is_superuser !== null) {
    2758            1 :             self.allowed = decide(self.user);
    2759            1 :         } else {
    2760            2 :             Promise.all([cockpit.user(), check_superuser()])
    2761            2 :                 .then(([user, is_superuser]) => {
    2762            2 :                     self.user = user;
    2763            2 :                     self.is_superuser = is_superuser;
    2764            2 :                     const allowed = decide(user);
    2765            2 :                     if (self.allowed !== allowed) {
    2766            2 :                         self.allowed = allowed;
    2767            2 :                         maybe_reload();
    2768            2 :                         self.dispatchEvent("changed");
    2769            2 :                     }
    2770            2 :                 });
    2771            2 :         }
    2772              : 
    2773            0 :         self.close = function close() {
    2774              :             /* no-op for now */
    2775            0 :         };
    2776            2 :     }
    2777              : 
    2778            2 :     cockpit.permission = function permission(arg) {
    2779            2 :         return new Permission(arg);
    2780            2 :     };
    2781              : 
    2782              :     /* ---------------------------------------------------------------------
    2783              :      * Metrics
    2784              :      *
    2785              :      */
    2786              : 
    2787          149 :     function MetricsChannel(interval, options_list, cache) {
    2788          149 :         const self = this;
    2789          149 :         event_mixin(self, { });
    2790              : 
    2791          149 :         if (options_list.length === undefined)
    2792           20 :             options_list = [options_list];
    2793              : 
    2794          149 :         const channels = [];
    2795          149 :         let following = false;
    2796              : 
    2797          149 :         self.series = cockpit.series(interval, cache, fetch_for_series);
    2798          149 :         self.archives = null;
    2799          149 :         self.meta = null;
    2800              : 
    2801          149 :         function fetch_for_series(beg, end, for_walking) {
    2802          149 :             if (!for_walking)
    2803          142 :                 self.fetch(beg, end);
    2804              :             else
    2805          142 :                 self.follow();
    2806          149 :         }
    2807              : 
    2808          149 :         function transfer(options_list, callback, is_archive) {
    2809          149 :             if (options_list.length === 0)
    2810          149 :                 return;
    2811              : 
    2812          144 :             if (!is_archive) {
    2813          144 :                 if (following)
    2814          144 :                     return;
    2815          144 :                 following = true;
    2816          144 :             }
    2817              : 
    2818          149 :             const options = {
    2819          149 :                 payload: "metrics1",
    2820          149 :                 interval,
    2821          149 :                 source: "internal",
    2822          149 :                 ...options_list[0]
    2823          149 :             };
    2824              : 
    2825          149 :             delete options.archive_source;
    2826              : 
    2827          149 :             const channel = cockpit.channel(options);
    2828          149 :             channels.push(channel);
    2829              : 
    2830          149 :             let meta = null;
    2831          149 :             let last = null;
    2832          149 :             let beg;
    2833              : 
    2834          149 :             channel.addEventListener("close", function(ev, close_options) {
    2835          149 :                 if (!is_archive)
    2836           20 :                     following = false;
    2837              : 
    2838          149 :                 if (options_list.length > 1 &&
    2839           19 :                     (close_options.problem == "not-supported" || close_options.problem == "not-found")) {
    2840           19 :                     transfer(options_list.slice(1), callback);
    2841           19 :                 } else if (close_options.problem) {
    2842          121 :                     if (close_options.problem != "terminated" &&
    2843          121 :                         close_options.problem != "disconnected" &&
    2844          121 :                         close_options.problem != "authentication-failed" &&
    2845          121 :                         (close_options.problem != "not-found" || !is_archive) &&
    2846           19 :                         (close_options.problem != "not-supported" || !is_archive)) {
    2847           19 :                         console.warn("metrics channel failed: " + close_options.problem);
    2848           19 :                     }
    2849           20 :                 } else if (is_archive) {
    2850           48 :                     if (!self.archives) {
    2851           48 :                         self.archives = true;
    2852           48 :                         self.dispatchEvent('changed');
    2853           48 :                     }
    2854           48 :                 }
    2855          149 :             });
    2856              : 
    2857          139 :             channel.addEventListener("message", function(ev, payload) {
    2858          139 :                 const message = JSON.parse(payload);
    2859              : 
    2860              :                 /* A meta message? */
    2861          139 :                 const message_len = message.length;
    2862          139 :                 if (message_len === undefined) {
    2863          139 :                     meta = message;
    2864          139 :                     let timestamp = 0;
    2865          139 :                     if (meta.now && meta.timestamp)
    2866          139 :                         timestamp = meta.timestamp + (Date.now() - meta.now);
    2867          139 :                     beg = Math.floor(timestamp / interval);
    2868          139 :                     callback(beg, meta, null, options_list[0]);
    2869              : 
    2870              :                     /* Trigger to outside interest that meta changed */
    2871          139 :                     self.meta = meta;
    2872          139 :                     self.dispatchEvent('changed');
    2873              : 
    2874              :                 /* A data message */
    2875          139 :                 } else if (meta) {
    2876              :                     /* Data decompression */
    2877          139 :                     for (let i = 0; i < message_len; i++) {
    2878          139 :                         const data = message[i];
    2879          134 :                         if (last) {
    2880          134 :                             for (let j = 0; j < last.length; j++) {
    2881          134 :                                 const dataj = data[j];
    2882           12 :                                 if (dataj === null || dataj === undefined) {
    2883           12 :                                     data[j] = last[j];
    2884           12 :                                 } else {
    2885          134 :                                     const dataj_len = dataj.length;
    2886          133 :                                     if (dataj_len !== undefined) {
    2887          133 :                                         const lastj = last[j];
    2888          133 :                                         const lastj_len = last[j].length;
    2889          133 :                                         let k;
    2890          133 :                                         for (k = 0; k < dataj_len; k++) {
    2891          133 :                                             if (dataj[k] === null)
    2892           12 :                                                 dataj[k] = lastj[k];
    2893          133 :                                         }
    2894          133 :                                         for (; k < lastj_len; k++)
    2895           23 :                                             dataj[k] = lastj[k];
    2896          133 :                                     }
    2897          134 :                                 }
    2898          134 :                             }
    2899          134 :                         }
    2900          139 :                         last = data;
    2901          139 :                     }
    2902              : 
    2903              :                     /* Return the data */
    2904          139 :                     callback(beg, meta, message, options_list[0]);
    2905              : 
    2906              :                     /* Bump timestamp for the next message */
    2907          139 :                     beg += message_len;
    2908          139 :                     meta.timestamp += (interval * message_len);
    2909          139 :                 }
    2910          139 :             });
    2911          149 :         }
    2912              : 
    2913          139 :         function drain(beg, meta, message, options) {
    2914              :             /* Generate a mapping object if necessary */
    2915          139 :             let mapping = meta.mapping;
    2916          139 :             if (!mapping) {
    2917          139 :                 mapping = { };
    2918          139 :                 meta.metrics.forEach(function(metric, i) {
    2919          139 :                     const map = { "": i };
    2920           13 :                     const name = options.metrics_path_names?.[i] ?? metric.name;
    2921          139 :                     mapping[name] = map;
    2922          138 :                     if (metric.instances) {
    2923          138 :                         metric.instances.forEach(function(instance, i) {
    2924          138 :                             if (instance === "")
    2925           12 :                                 instance = "/";
    2926          138 :                             map[instance] = { "": i };
    2927          138 :                         });
    2928          138 :                     }
    2929          139 :                 });
    2930          139 :                 meta.mapping = mapping;
    2931          139 :             }
    2932              : 
    2933          139 :             if (message)
    2934          139 :                 self.series.input(beg, message, mapping);
    2935          139 :         }
    2936              : 
    2937          149 :         self.fetch = function fetch(beg, end) {
    2938          149 :             const timestamp = beg * interval - Date.now();
    2939          149 :             const limit = end - beg;
    2940              : 
    2941          149 :             const archive_options_list = [];
    2942          149 :             for (let i = 0; i < options_list.length; i++) {
    2943          149 :                 if (options_list[i].archive_source) {
    2944          149 :                     archive_options_list.push({
    2945          149 :                                                    ...options_list[i],
    2946          149 :                                                    source: options_list[i].archive_source,
    2947          149 :                                                    timestamp,
    2948          149 :                                                    limit
    2949          149 :                                               });
    2950          149 :                 }
    2951          149 :             }
    2952              : 
    2953          149 :             transfer(archive_options_list, drain, true);
    2954          149 :         };
    2955              : 
    2956          139 :         self.follow = function follow() {
    2957          139 :             transfer(options_list, drain);
    2958          139 :         };
    2959              : 
    2960            1 :         self.close = function close(options) {
    2961            1 :             const len = channels.length;
    2962            1 :             if (self.series)
    2963            1 :                 self.series.close();
    2964              : 
    2965            1 :             for (let i = 0; i < len; i++)
    2966            1 :                 channels[i].close(options);
    2967            1 :         };
    2968          149 :     }
    2969              : 
    2970          149 :     cockpit.metrics = function metrics(interval, options) {
    2971          149 :         return new MetricsChannel(interval, options);
    2972          149 :     };
    2973              : 
    2974              :     /* ---------------------------------------------------------------------
    2975              :      * Ooops handling.
    2976              :      *
    2977              :      * If we're embedded, send oops to parent frame. Since everything
    2978              :      * could be broken at this point, just do it manually, without
    2979              :      * involving cockpit.transport or any of that logic.
    2980              :      */
    2981              : 
    2982            1 :     cockpit.oops = function oops() {
    2983            1 :         if (window.parent !== window && window.name.indexOf("cockpit1:") === 0)
    2984            1 :             window.parent.postMessage("\n{ \"command\": \"oops\" }", transport_origin);
    2985            1 :     };
    2986              : 
    2987          368 :     const old_onerror = window.onerror;
    2988            1 :     window.onerror = function(msg, url, line) {
    2989              :         // Errors with url == "" are not logged apparently, so let's
    2990              :         // not show the "Oops" for them either.
    2991            1 :         if (url != "")
    2992            1 :             cockpit.oops();
    2993            1 :         if (old_onerror)
    2994            0 :             return old_onerror(msg, url, line);
    2995            1 :         return false;
    2996            1 :     };
    2997              : 
    2998          343 :     cockpit.assert = (predicate, message) => {
    2999           63 :         if (!predicate) {
    3000           63 :             throw new Error(`Assertion failed: ${message}`);
    3001           63 :         }
    3002          343 :     };
    3003              : 
    3004          368 :     return cockpit;
    3005          368 : }
    3006              : 
    3007          368 : const cockpit = factory();
    3008            3 : export default cockpit;
    3009              : 
    3010              : // Register cockpit object as global, so that it can be used without ES6 modules
    3011              : // we need to do that here instead of in pkg/base1/cockpit.js, so that po.js can access cockpit already
    3012          368 : window.cockpit = cockpit;
        

Generated by: LCOV version 2.0-1