LCOV - code coverage report
Current view: top level - pkg/lib - cockpit.js Coverage Total Hit
Test: cockpit Lines: 70.7 % 2219 1569
Test Date: 2026-06-17 06:28:00

            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          127 : function factory() {
      22          127 :     const cockpit = { };
      23          127 :     event_mixin(cockpit, { });
      24              : 
      25            2 :     cockpit.init = async () => {
      26            2 :         await new Promise(resolve => ensure_transport(resolve));
      27            2 :         Object.assign(cockpit.info, await fetch_info());
      28            2 :     };
      29              : 
      30          127 :     cockpit.channel = function channel(options) {
      31          127 :         return new Channel(options);
      32          127 :     };
      33              : 
      34          126 :     cockpit.event_target = function event_target(obj) {
      35          126 :         event_mixin(obj, { });
      36          126 :         return obj;
      37          126 :     };
      38              : 
      39              :     /* obsolete backwards compatible shim */
      40          127 :     cockpit.extend = Object.assign;
      41              : 
      42              :     /* These can be filled in by loading ../manifests.js */
      43          127 :     cockpit.manifests = { };
      44              : 
      45              :     /* ------------------------------------------------------------
      46              :      * Text Encoding
      47              :      */
      48              : 
      49          127 :     cockpit.base64_encode = base64_encode;
      50          127 :     cockpit.base64_decode = base64_decode;
      51              : 
      52           20 :     cockpit.kill = function kill(host, group) {
      53           20 :         const options = { };
      54           20 :         if (host)
      55            6 :             options.host = host;
      56           20 :         if (group)
      57           20 :             options.group = group;
      58           20 :         cockpit.transport.control("kill", options);
      59           20 :     };
      60              : 
      61              :     /* Not public API ... yet? */
      62           45 :     cockpit.hint = function hint(name, options) {
      63           45 :         if (!transport_globals.default_transport)
      64           45 :             return;
      65           45 :         if (!options)
      66            4 :             options = transport_globals.default_host;
      67           45 :         if (typeof options == "string")
      68            4 :             options = { host: options };
      69           45 :         options.hint = name;
      70           45 :         cockpit.transport.control("hint", options);
      71           45 :     };
      72              : 
      73          127 :     cockpit.transport = {
      74          127 :         wait: ensure_transport,
      75          120 :         inject: function inject(message, out) {
      76          120 :             if (!transport_globals.default_transport)
      77           19 :                 return false;
      78          120 :             if (out === undefined || out)
      79           19 :                 return transport_globals.default_transport.send_data(message);
      80              :             else
      81           19 :                 return transport_globals.default_transport.dispatch_data({ data: message });
      82          120 :         },
      83          123 :         filter: function filter(callback, out) {
      84           19 :             if (out) {
      85           19 :                 console.error("'out' filters are no longer supported");
      86           19 :             } else {
      87          123 :                 transport_globals.incoming_filters.push(callback);
      88          123 :             }
      89          123 :         },
      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          127 :         origin: transport_origin,
      97          127 :         options: { },
      98          127 :         uri: calculate_url,
      99           66 :         control: function(command, options) {
     100           66 :             options = { ...options, command };
     101           66 :             ensure_transport(function(transport) {
     102           66 :                 transport.send_control(options);
     103           66 :             });
     104           66 :         },
     105          123 :         application: function () {
     106          123 :             if (!transport_globals.default_transport || window.mock)
     107           19 :                 return calculate_application();
     108          123 :             return transport_globals.default_transport.application;
     109          123 :         },
     110          127 :     };
     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          127 :     cockpit.defer = function() {
     123          127 :         return new Deferred();
     124          127 :     };
     125              : 
     126              :     /* ---------------------------------------------------------------------
     127              :      * Utilities
     128              :      */
     129              : 
     130          127 :     const fmt_re = /\$\{([^}]+)\}|\$([a-zA-Z0-9_]+)/g;
     131          123 :     cockpit.format = function format(fmt, args) {
     132           19 :         if (arguments.length != 2 || !is_object(args) || args === null)
     133          123 :             args = Array.prototype.slice.call(arguments, 1);
     134              : 
     135          123 :         function replace(m, x, y) {
     136          123 :             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          123 :             if (value === 0)
     142           20 :                 return '0';
     143              : 
     144           29 :             return value || '';
     145          123 :         }
     146              : 
     147          123 :         return fmt.replace(fmt_re, replace);
     148          123 :     };
     149              : 
     150           37 :     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           37 :         if (precision === undefined)
     161           26 :             precision = 3;
     162            3 :         const lang = cockpit.language === undefined ? undefined : cockpit.language.replace('_', '-');
     163           37 :         const smallestValue = 10 ** (-precision);
     164              : 
     165           13 :         if (!number && number !== 0)
     166           13 :             return "";
     167           37 :         else if (number % 1 === 0)
     168            4 :             return number.toString();
     169           37 :         else if (number > 0 && number <= smallestValue)
     170            3 :             return smallestValue.toLocaleString(lang);
     171            3 :         else if (number < 0 && number >= -smallestValue)
     172            3 :             return (-smallestValue).toLocaleString(lang);
     173           37 :         else if (number > 999 || number < -999)
     174            3 :             return number.toFixed(0);
     175              :         else
     176           37 :             return number.toLocaleString(lang, {
     177           37 :                 maximumSignificantDigits: precision,
     178           37 :                 minimumSignificantDigits: precision,
     179           37 :             });
     180           37 :     };
     181              : 
     182          127 :     let deprecated_format_warned = false;
     183           37 :     function format_units(suffixes, number, second_arg, third_arg) {
     184           37 :         let options = second_arg;
     185           15 :         let factor = options?.base2 ? 1024 : 1000;
     186              : 
     187              :         // compat API: we used to accept 'factor' as a separate second arg
     188           15 :         if (third_arg || (second_arg && !is_object(second_arg))) {
     189           16 :             if (!deprecated_format_warned) {
     190           16 :                 console.warn(`cockpit.format_{bytes,bits}[_per_sec](..., ${second_arg}, ${third_arg}) is deprecated.`);
     191           16 :                 deprecated_format_warned = true;
     192           16 :             }
     193              : 
     194            3 :             factor = second_arg || 1000;
     195           16 :             options = third_arg;
     196              :             // double backwards compat: "options" argument position used to be a boolean flag "separate"
     197           16 :             if (!is_object(options))
     198            4 :                 options = { separate: options };
     199           16 :         }
     200              : 
     201           37 :         let suffix = null;
     202              : 
     203              :         /* Find that factor string */
     204           13 :         if (!number && number !== 0) {
     205           13 :             suffix = null;
     206            4 :         } else if (typeof (factor) === "string") {
     207              :             /* Prefer larger factors */
     208           16 :             const keys = [];
     209           16 :             for (const key in suffixes)
     210           16 :                 keys.push(key);
     211           16 :             keys.sort().reverse();
     212           16 :             for (let y = 0; y < keys.length; y++) {
     213           16 :                 for (let x = 0; x < suffixes[keys[y]].length; x++) {
     214           16 :                     if (factor == suffixes[keys[y]][x]) {
     215           16 :                         number = number / Math.pow(keys[y], x);
     216           16 :                         suffix = factor;
     217           16 :                         break;
     218           16 :                     }
     219           16 :                 }
     220           16 :                 if (suffix)
     221           16 :                     break;
     222           16 :             }
     223              : 
     224              :         /* @factor is a number */
     225           16 :         } else if (factor in suffixes) {
     226           37 :             let divisor = 1;
     227           37 :             for (let i = 0; i < suffixes[factor].length; i++) {
     228           37 :                 const quotient = number / divisor;
     229           37 :                 if (quotient < factor) {
     230           37 :                     number = quotient;
     231           37 :                     suffix = suffixes[factor][i];
     232           37 :                     break;
     233           37 :                 }
     234           37 :                 divisor *= factor;
     235           37 :             }
     236           37 :         }
     237              : 
     238           17 :         const string_representation = cockpit.format_number(number, options?.precision);
     239           37 :         let ret;
     240              : 
     241           37 :         if (string_representation && suffix)
     242           13 :             ret = [string_representation, suffix];
     243              :         else
     244           13 :             ret = [string_representation];
     245              : 
     246           17 :         if (!options?.separate)
     247           26 :             ret = ret.join(" ");
     248              : 
     249           37 :         return ret;
     250           37 :     }
     251              : 
     252          127 :     const byte_suffixes = {
     253          127 :         1000: ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB"],
     254          127 :         1024: ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"]
     255          127 :     };
     256              : 
     257           37 :     cockpit.format_bytes = function format_bytes(number, ...args) {
     258           37 :         return format_units(byte_suffixes, number, ...args);
     259           37 :     };
     260              : 
     261          127 :     const byte_sec_suffixes = {
     262          127 :         1000: ["B/s", "kB/s", "MB/s", "GB/s", "TB/s", "PB/s", "EB/s", "ZB/s"],
     263          127 :         1024: ["B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s", "PiB/s", "EiB/s", "ZiB/s"]
     264          127 :     };
     265              : 
     266            8 :     cockpit.format_bytes_per_sec = function format_bytes_per_sec(number, ...args) {
     267            8 :         return format_units(byte_sec_suffixes, number, ...args);
     268            8 :     };
     269              : 
     270          127 :     const bit_suffixes = {
     271          127 :         1000: ["bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps"]
     272          127 :     };
     273              : 
     274            1 :     cockpit.format_bits_per_sec = function format_bits_per_sec(number, ...args) {
     275            1 :         return format_units(bit_suffixes, number, ...args);
     276            1 :     };
     277              : 
     278              :     /* ---------------------------------------------------------------------
     279              :      * Storage Helper.
     280              :      *
     281              :      * Use application to prefix data stored in browser storage
     282              :      * with helpers for compatibility.
     283              :      */
     284          127 :     function StorageHelper(storageName) {
     285          127 :         const self = this;
     286          127 :         let storage;
     287              : 
     288          127 :         try {
     289          127 :             storage = window[storageName];
     290           19 :         } catch (e) { }
     291              : 
     292          123 :         self.prefixedKey = function (key) {
     293          123 :             return cockpit.transport.application() + ":" + key;
     294          123 :         };
     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          127 :     }
     331              : 
     332          127 :     cockpit.localStorage = new StorageHelper("localStorage");
     333          127 :     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            1 :     function SeriesSink(interval, identifier, fetch_callback) {
     506            1 :         const self = this;
     507              : 
     508            1 :         self.interval = interval;
     509            0 :         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            1 :         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            1 :         let count = 0;
     526            1 :         let head = null;
     527            1 :         let tail = null;
     528              : 
     529            1 :         function setup_index(id) {
     530            1 :             if (!id)
     531            1 :                 return [];
     532              : 
     533              :             /* Try and find a good place to cache data */
     534            0 :             const storage = lookup_storage(window);
     535              : 
     536            0 :             let index = storage[id];
     537            0 :             if (!index)
     538            0 :                 storage[id] = index = [];
     539            0 :             return index;
     540            1 :         }
     541              : 
     542            1 :         function search(idx, beg) {
     543            1 :             let low = 0;
     544            1 :             let high = idx.length - 1;
     545              : 
     546            1 :             while (low <= high) {
     547            1 :                 const mid = (low + high) / 2 | 0;
     548            1 :                 const val = idx[mid].beg;
     549            1 :                 if (val < beg)
     550            1 :                     low = mid + 1;
     551            1 :                 else if (val > beg)
     552            0 :                     high = mid - 1;
     553              :                 else
     554            1 :                     return mid; /* key found */
     555            1 :             }
     556            1 :             return low;
     557            1 :         }
     558              : 
     559            1 :         function fetch(beg, end, for_walking) {
     560            1 :             if (fetch_callback) {
     561            1 :                 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            1 :                     stash(beg, new Array(end - beg), { });
     567            1 :                 }
     568            1 :                 fetch_callback(beg, end, for_walking);
     569            1 :             }
     570            1 :         }
     571              : 
     572            1 :         self.load = function load(beg, end, for_walking) {
     573            1 :             if (end <= beg)
     574            1 :                 return;
     575              : 
     576            1 :             const at = search(index, beg);
     577              : 
     578            1 :             const len = index.length;
     579            1 :             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            1 :             const fetches = [];
     591              : 
     592              :             /* Data relevant to this range can be at the found index, or earlier */
     593            0 :             for (let i = at > 0 ? at - 1 : at; i < len; i++) {
     594            1 :                 const entry = index[i];
     595            1 :                 const en = entry.items.length;
     596            1 :                 if (!en)
     597            1 :                     continue;
     598              : 
     599            1 :                 const eb = entry.beg;
     600            1 :                 const b = Math.max(eb, beg);
     601            1 :                 const e = Math.min(eb + en, end);
     602              : 
     603            1 :                 if (b < e) {
     604            1 :                     if (b > last)
     605            0 :                         fetches.push([last, b]);
     606            1 :                     process(b, entry.items.slice(b - eb, e - eb), entry.mapping);
     607            1 :                     last = e;
     608            0 :                 } else if (i >= at) {
     609            0 :                     break; /* no further intersections */
     610            0 :                 }
     611            1 :             }
     612              : 
     613            1 :             for (let i = 0; i < fetches.length; i++)
     614            0 :                 fetch(fetches[i][0], fetches[i][1], for_walking);
     615              : 
     616            1 :             if (last != end)
     617            1 :                 fetch(last, end, for_walking);
     618            1 :         };
     619              : 
     620            1 :         function stash(beg, items, mapping) {
     621            1 :             if (!items.length)
     622            1 :                 return;
     623              : 
     624            1 :             let at = search(index, beg);
     625              : 
     626            1 :             const end = beg + items.length;
     627              : 
     628            1 :             const len = index.length;
     629            1 :             let i;
     630            1 :             for (i = at > 0 ? at - 1 : at; i < len; i++) {
     631            1 :                 const entry = index[i];
     632            1 :                 const en = entry.items.length;
     633            1 :                 if (!en)
     634            1 :                     continue;
     635              : 
     636            1 :                 const eb = entry.beg;
     637            1 :                 const b = Math.max(eb, beg);
     638            1 :                 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            0 :                 if (b < e) {
     649            0 :                     const num = e - b;
     650            0 :                     entry.items.splice(b - eb, num);
     651            0 :                     count -= num;
     652            0 :                     if (b - eb === 0)
     653            0 :                         entry.beg += (e - eb);
     654            0 :                 } else if (i >= at) {
     655            0 :                     break; /* no further intersections */
     656            0 :                 }
     657            1 :             }
     658              : 
     659              :             /* Insert our item into the array */
     660            1 :             const entry = { beg, items, mapping };
     661            1 :             if (!head)
     662            1 :                 head = entry;
     663            1 :             if (tail)
     664            1 :                 tail.next = entry;
     665            1 :             tail = entry;
     666            1 :             count += items.length;
     667            1 :             index.splice(at, 0, entry);
     668              : 
     669              :             /* Remove any items with zero length around insertion point */
     670            1 :             for (at--; at <= i; at++) {
     671            1 :                 const entry = index[at];
     672            0 :                 if (entry && !entry.items.length) {
     673            0 :                     index.splice(at, 1);
     674            0 :                     at--;
     675            0 :                 }
     676            1 :             }
     677              : 
     678              :             /* If our index has gotten too big, expire entries */
     679            0 :             while (head && count > self.limit) {
     680            0 :                 count -= head.items.length;
     681            0 :                 head.items = [];
     682            0 :                 head.mapping = null;
     683            0 :                 head = head.next || null;
     684            0 :             }
     685              : 
     686              :             /* Remove any entries with zero length at beginning */
     687            1 :             const newlen = index.length;
     688            1 :             for (i = 0; i < newlen; i++) {
     689            1 :                 if (index[i].items.length > 0)
     690            1 :                     break;
     691            1 :             }
     692            1 :             index.splice(0, i);
     693            1 :         }
     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            1 :         const registered = { };
     703              : 
     704              :         /* An undocumented function called by DataGrid */
     705            1 :         self._register = function _register(grid, id) {
     706            1 :             if (grid.interval != interval)
     707            0 :                 throw Error("mismatched metric interval between grid and sink");
     708            1 :             let gdata = registered[id];
     709            1 :             if (!gdata) {
     710            1 :                 gdata = registered[id] = { grid, links: [] };
     711            0 :                 gdata.links.remove = function remove() {
     712            0 :                     delete registered[id];
     713            0 :                 };
     714            1 :             }
     715            1 :             return gdata.links;
     716            1 :         };
     717              : 
     718            1 :         function process(beg, items, mapping) {
     719            1 :             const end = beg + items.length;
     720              : 
     721            1 :             for (const id in registered) {
     722            1 :                 const gdata = registered[id];
     723            1 :                 const grid = gdata.grid;
     724              : 
     725            1 :                 const b = Math.max(beg, grid.beg);
     726            1 :                 const e = Math.min(end, grid.end);
     727              : 
     728              :                 /* Does this grid overlap the bounds of item? */
     729            1 :                 if (b < e) {
     730              :                     /* Where in the items to take from */
     731            1 :                     const f = b - beg;
     732              : 
     733              :                     /* Where and how many to place */
     734            1 :                     const t = b - grid.beg;
     735              : 
     736              :                     /* How many to process */
     737            1 :                     const n = e - b;
     738              : 
     739            1 :                     for (let i = 0; i < n; i++) {
     740            1 :                         const klen = gdata.links.length;
     741            1 :                         for (let k = 0; k < klen; k++) {
     742            1 :                             const path = gdata.links[k][0];
     743            1 :                             const row = gdata.links[k][1];
     744              : 
     745              :                             /* Calculate the data field to fill in */
     746            1 :                             let data = items[f + i];
     747            1 :                             let map = mapping;
     748            1 :                             const jlen = path.length;
     749            0 :                             for (let j = 0; data !== undefined && j < jlen; j++) {
     750            0 :                                 if (!data) {
     751            0 :                                     data = undefined;
     752            0 :                                 } else if (map !== undefined && map !== null) {
     753            0 :                                     map = map[path[j]];
     754            0 :                                     if (map)
     755            0 :                                         data = data[map[""]];
     756              :                                     else
     757            0 :                                         data = data[path[j]];
     758            0 :                                 } else {
     759            0 :                                     data = data[path[j]];
     760            0 :                                 }
     761            0 :                             }
     762              : 
     763            1 :                             row[t + i] = data;
     764            1 :                         }
     765            1 :                     }
     766              : 
     767              :                     /* Notify the grid, so it can call any functions */
     768            1 :                     grid.notify(t, n);
     769            1 :                 }
     770            1 :             }
     771            1 :         }
     772              : 
     773            1 :         self.input = function input(beg, items, mapping) {
     774            1 :             process(beg, items, mapping);
     775            1 :             stash(beg, items, mapping);
     776            1 :         };
     777              : 
     778            0 :         self.close = function () {
     779            0 :             for (const id in registered) {
     780            0 :                 const grid = registered[id];
     781            0 :                 if (grid?.grid)
     782            0 :                     grid.grid.remove_sink(self);
     783            0 :             }
     784            0 :         };
     785            1 :     }
     786              : 
     787            1 :     cockpit.series = function series(interval, cache, fetch) {
     788            1 :         return new SeriesSink(interval, cache, fetch);
     789            1 :     };
     790              : 
     791          127 :     let unique = 1;
     792              : 
     793            1 :     function SeriesGrid(interval, beg, end) {
     794            1 :         const self = this;
     795              : 
     796              :         /* We can trigger events */
     797            1 :         event_mixin(self, { });
     798              : 
     799            1 :         const rows = [];
     800              : 
     801            1 :         self.interval = interval;
     802            1 :         self.beg = 0;
     803            1 :         self.end = 0;
     804              : 
     805              :         /*
     806              :          * Used to populate table data, the values are:
     807              :          * [ callback, row ]
     808              :          */
     809            1 :         const callbacks = [];
     810              : 
     811            1 :         const sinks = [];
     812              : 
     813            1 :         let suppress = 0;
     814              : 
     815            1 :         const id = "g1-" + unique;
     816            1 :         unique += 1;
     817              : 
     818              :         /* Used while walking */
     819            1 :         let walking = null;
     820            1 :         let offset = null;
     821              : 
     822            1 :         self.notify = function notify(x, n) {
     823            1 :             if (suppress)
     824            1 :                 return;
     825            1 :             if (x + n > self.end - self.beg)
     826            0 :                 n = (self.end - self.beg) - x;
     827            1 :             if (n <= 0)
     828            1 :                 return;
     829            1 :             const jlen = callbacks.length;
     830            1 :             for (let j = 0; j < jlen; j++) {
     831            1 :                 const callback = callbacks[j][0];
     832            1 :                 const row = callbacks[j][1];
     833            1 :                 callback.call(self, row, x, n);
     834            1 :             }
     835              : 
     836            1 :             self.dispatchEvent("notify", x, n);
     837            1 :         };
     838              : 
     839            1 :         self.add = function add(/* sink, path */) {
     840            1 :             const row = [];
     841            1 :             rows.push(row);
     842              : 
     843              :             /* Called as add(sink, path) */
     844            1 :             if (is_object(arguments[0])) {
     845            0 :                 const sink = arguments[0].series || arguments[0];
     846              : 
     847              :                 /* The path argument can be an array, or a dot separated string */
     848            1 :                 let path = arguments[1];
     849            1 :                 if (!path)
     850            0 :                     path = [];
     851            1 :                 else if (typeof (path) === "string")
     852            0 :                     path = path.split(".");
     853              : 
     854            1 :                 const links = sink._register(self, id);
     855            1 :                 if (!links.length)
     856            1 :                     sinks.push({ sink, links });
     857            1 :                 links.push([path, row]);
     858              : 
     859              :             /* Called as add(callback) */
     860            1 :             } else if (is_function(arguments[0])) {
     861            1 :                 const cb = [arguments[0], row];
     862            1 :                 if (arguments[1] === true)
     863            0 :                     callbacks.unshift(cb);
     864              :                 else
     865            1 :                     callbacks.push(cb);
     866              : 
     867              :             /* Not called as add() */
     868            0 :             } else if (arguments.length !== 0) {
     869            0 :                 throw Error("invalid args to grid.add()");
     870            0 :             }
     871              : 
     872            1 :             return row;
     873            1 :         };
     874              : 
     875            1 :         self.remove = function remove(row) {
     876              :             /* Remove from the sinks */
     877            1 :             let ilen = sinks.length;
     878            1 :             for (let i = 0; i < ilen; i++) {
     879            1 :                 const jlen = sinks[i].links.length;
     880            1 :                 for (let j = 0; j < jlen; j++) {
     881            1 :                     if (sinks[i].links[j][1] === row) {
     882            1 :                         sinks[i].links.splice(j, 1);
     883            1 :                         break;
     884            1 :                     }
     885            1 :                 }
     886            1 :             }
     887              : 
     888              :             /* Remove from our list of rows */
     889            1 :             ilen = rows.length;
     890            1 :             for (let i = 0; i < ilen; i++) {
     891            1 :                 if (rows[i] === row) {
     892            1 :                     rows.splice(i, 1);
     893            1 :                     break;
     894            1 :                 }
     895            1 :             }
     896            1 :         };
     897              : 
     898            0 :         self.remove_sink = function remove_sink(sink) {
     899            0 :             const len = sinks.length;
     900            0 :             for (let i = 0; i < len; i++) {
     901            0 :                 if (sinks[i].sink === sink) {
     902            0 :                     sinks[i].links.remove();
     903            0 :                     sinks.splice(i, 1);
     904            0 :                     break;
     905            0 :                 }
     906            0 :             }
     907            0 :         };
     908              : 
     909            1 :         self.sync = function sync(for_walking) {
     910              :             /* Suppress notifications */
     911            1 :             suppress++;
     912              : 
     913              :             /* Ask all sinks to load data */
     914            1 :             const len = sinks.length;
     915            1 :             for (let i = 0; i < len; i++) {
     916            1 :                 const sink = sinks[i].sink;
     917            1 :                 sink.load(self.beg, self.end, for_walking);
     918            1 :             }
     919              : 
     920            1 :             suppress--;
     921              : 
     922              :             /* Notify for all rows */
     923            1 :             self.notify(0, self.end - self.beg);
     924            1 :         };
     925              : 
     926            1 :         function move_internal(beg, end, for_walking) {
     927            1 :             if (end === undefined)
     928            1 :                 end = beg + (self.end - self.beg);
     929              : 
     930            1 :             if (end < beg)
     931            0 :                 beg = end;
     932              : 
     933            1 :             self.beg = beg;
     934            1 :             self.end = end;
     935              : 
     936            1 :             if (!rows.length)
     937            1 :                 return;
     938              : 
     939            0 :             rows.forEach(function(row) {
     940            0 :                 row.length = 0;
     941            0 :             });
     942              : 
     943            0 :             self.sync(for_walking);
     944            1 :         }
     945              : 
     946            1 :         function stop_walking() {
     947            1 :             window.clearInterval(walking);
     948            1 :             walking = null;
     949            1 :             offset = null;
     950            1 :         }
     951              : 
     952            1 :         function is_negative(n) {
     953            1 :             return ((n = +n) || 1 / n) < 0;
     954            1 :         }
     955              : 
     956            1 :         self.move = function move(beg, end) {
     957            1 :             stop_walking();
     958              :             /* Some code paths use now twice.
     959              :              * They should use the same value.
     960              :              */
     961            1 :             let now = null;
     962              : 
     963              :             /* Treat negative numbers relative to now */
     964            0 :             if (beg === undefined) {
     965            0 :                 beg = 0;
     966            0 :             } else if (is_negative(beg)) {
     967            1 :                 now = Date.now();
     968            1 :                 beg = Math.floor(now / self.interval) + beg;
     969            1 :             }
     970            1 :             if (end !== undefined && is_negative(end)) {
     971            1 :                 if (now === null)
     972            0 :                     now = Date.now();
     973            1 :                 end = Math.floor(now / self.interval) + end;
     974            1 :             }
     975              : 
     976            1 :             move_internal(beg, end, false);
     977            1 :         };
     978              : 
     979            1 :         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            1 :             const start = Date.now();
     997            1 :             if (self.interval > 2000000000)
     998            1 :                 return;
     999              : 
    1000            1 :             stop_walking();
    1001            1 :             offset = start - self.beg * self.interval;
    1002            1 :             walking = window.setInterval(function() {
    1003            1 :                 const now = Date.now();
    1004            1 :                 move_internal(Math.floor((now - offset) / self.interval), undefined, true);
    1005            1 :             }, self.interval);
    1006            1 :         };
    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            1 :         self.move(beg, end);
    1015            1 :     }
    1016              : 
    1017            1 :     cockpit.grid = function grid(interval, beg, end) {
    1018            1 :         return new SeriesGrid(interval, beg, end);
    1019            1 :     };
    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          127 :     cockpit.info = { };
    1066          127 :     event_mixin(cockpit.info, { });
    1067              : 
    1068          124 :     transport_globals.init_callback = function(options) {
    1069          124 :         if (options.system) {
    1070          124 :             cockpit.info.ws = options.system;
    1071          124 :             Object.assign(cockpit.info, options.system);
    1072          124 :         }
    1073          124 :         if (options.system)
    1074          124 :             cockpit.info.dispatchEvent("changed");
    1075              : 
    1076          124 :         cockpit.transport.options = options;
    1077          124 :         cockpit.transport.csrf_token = options["csrf-token"];
    1078          124 :         cockpit.transport.host = transport_globals.default_host;
    1079          124 :     };
    1080              : 
    1081          127 :     let the_user = null;
    1082          127 :     cockpit.user = function () {
    1083          127 :             if (!the_user) {
    1084          127 :                 const dbus = cockpit.dbus(null, { bus: "internal" });
    1085          127 :                 return dbus.call("/user", "org.freedesktop.DBus.Properties", "GetAll",
    1086          127 :                           ["cockpit.User"], { type: "s" })
    1087          124 :                     .then(([user]) => {
    1088          124 :                         the_user = {
    1089          124 :                             id: user.Id.v,
    1090          124 :                             gid: user.Gid?.v,
    1091          124 :                             name: user.Name.v,
    1092          124 :                             full_name: user.Full.v,
    1093          124 :                             groups: user.Groups.v,
    1094          124 :                             home: user.Home.v,
    1095          124 :                             shell: user.Shell.v
    1096          124 :                         };
    1097          124 :                         Object.freeze(the_user);
    1098          124 :                         return the_user;
    1099          124 :                     })
    1100          127 :                     .finally(() => dbus.close());
    1101           23 :             } else {
    1102           23 :                 return Promise.resolve(the_user);
    1103           23 :             }
    1104          127 :     };
    1105              : 
    1106              :     /* ------------------------------------------------------------------------
    1107              :      * Override for broken browser behavior
    1108              :      */
    1109              : 
    1110           98 :     document.addEventListener("click", function(ev) {
    1111           98 :         if (ev.target.classList && in_array(ev.target.classList, 'disabled'))
    1112           10 :           ev.stopPropagation();
    1113           98 :     }, true);
    1114              : 
    1115              :     /* ------------------------------------------------------------------------
    1116              :      * Cockpit location
    1117              :      */
    1118              : 
    1119          127 :     let last_loc = null;
    1120              : 
    1121          127 :     Object.defineProperty(cockpit, "location", {
    1122          127 :         enumerable: true,
    1123          134 :         get: function() {
    1124          131 :             if (!last_loc || last_loc.href !== window.location.hash.slice(1))
    1125          134 :                 last_loc = new Location();
    1126          134 :             return last_loc;
    1127          134 :         },
    1128            0 :         set: function(v) {
    1129            0 :             cockpit.location.go(v);
    1130            0 :         }
    1131          127 :     });
    1132              : 
    1133           44 :     window.addEventListener("hashchange", function() {
    1134           44 :         if (last_loc)
    1135           43 :             last_loc.invalidate();
    1136           44 :         last_loc = null;
    1137           44 :         const hash = window.location.hash.slice(1);
    1138           44 :         cockpit.hint("location", { hash });
    1139           44 :         cockpit.dispatchEvent("locationchanged");
    1140           44 :     });
    1141              : 
    1142              :     /* ------------------------------------------------------------------------
    1143              :      * Cockpit jump
    1144              :      */
    1145              : 
    1146           15 :     cockpit.jump = function jump(path, host) {
    1147           15 :         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           15 :             path = "" + path;
    1154              : 
    1155              :         /* When host is not given (undefined), use current transport's host. If
    1156              :          * it is null, use localhost.
    1157              :          */
    1158           15 :         if (host === undefined)
    1159           12 :             host = cockpit.transport.host;
    1160              : 
    1161           15 :         const options = { command: "jump", location: path, host };
    1162           15 :         cockpit.transport.inject("\n" + JSON.stringify(options));
    1163           15 :     };
    1164              : 
    1165              :     /* ---------------------------------------------------------------------
    1166              :      * Cockpit Page Visibility
    1167              :      */
    1168              : 
    1169          127 :     (function() {
    1170          127 :         let hiddenHint = false;
    1171              : 
    1172          138 :         function visibility_change() {
    1173          138 :             let value = document.hidden;
    1174          138 :             if (value === false)
    1175          138 :                 value = hiddenHint;
    1176          138 :             if (cockpit.hidden !== value) {
    1177          138 :                 cockpit.hidden = value;
    1178          138 :                 cockpit.dispatchEvent("visibilitychange");
    1179          138 :             }
    1180          138 :         }
    1181              : 
    1182          127 :         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          120 :         transport_globals.process_hints = function(data) {
    1190          120 :             if ("hidden" in data) {
    1191          120 :                 hiddenHint = data.hidden;
    1192          120 :                 visibility_change();
    1193          120 :             }
    1194          120 :         };
    1195              : 
    1196              :         /* The first time */
    1197          127 :         visibility_change();
    1198          127 :     }());
    1199              : 
    1200              :     /* ---------------------------------------------------------------------
    1201              :      * Spawning
    1202              :      */
    1203              : 
    1204           69 :     function ProcessError(options, name) {
    1205           47 :         this.problem = options.problem || null;
    1206           69 :         this.exit_status = options["exit-status"];
    1207           69 :         if (this.exit_status === undefined)
    1208           35 :             this.exit_status = null;
    1209           69 :         this.exit_signal = options["exit-signal"];
    1210           69 :         if (this.exit_signal === undefined)
    1211           69 :             this.exit_signal = null;
    1212           69 :         this.message = options.message;
    1213              : 
    1214           35 :         if (this.message === undefined) {
    1215           35 :             if (this.problem)
    1216           10 :                 this.message = cockpit.message(options.problem);
    1217           10 :             else if (this.exit_signal !== null)
    1218           10 :                 this.message = cockpit.format(_("$0 killed with signal $1"), name, this.exit_signal);
    1219           10 :             else if (this.exit_status !== null)
    1220           10 :                 this.message = cockpit.format(_("$0 exited with code $1"), name, this.exit_status);
    1221              :             else
    1222           10 :                 this.message = cockpit.format(_("$0 failed"), name);
    1223           13 :         } else {
    1224           47 :             this.message = this.message.trim();
    1225           47 :         }
    1226              : 
    1227            8 :         this.toString = function() {
    1228            8 :             return this.message;
    1229            8 :         };
    1230           69 :     }
    1231              : 
    1232          127 :     cockpit.ProcessError = ProcessError;
    1233              : 
    1234          123 :     function spawn_debug() {
    1235           22 :         if (window.debugging == "all" || window.debugging?.includes("spawn"))
    1236           19 :             console.debug.apply(console, arguments);
    1237          123 :     }
    1238              : 
    1239              :     /* public */
    1240          123 :     cockpit.spawn = function(command, options) {
    1241          123 :         const dfd = cockpit.defer();
    1242              : 
    1243          123 :         const args = { payload: "stream", spawn: [] };
    1244          123 :         if (command instanceof Array) {
    1245          123 :             for (let i = 0; i < command.length; i++)
    1246          123 :                 args.spawn.push(String(command[i]));
    1247           20 :         } else {
    1248           20 :             args.spawn.push(String(command));
    1249           20 :         }
    1250          123 :         if (options !== undefined)
    1251          115 :             Object.assign(args, options);
    1252              : 
    1253          123 :         spawn_debug("process spawn:", JSON.stringify(args.spawn));
    1254              : 
    1255           19 :         const name = args.spawn[0] || "process";
    1256          123 :         const channel = cockpit.channel(args);
    1257              : 
    1258              :         /* Callback that wants a stream response, see below */
    1259          123 :         const buffer = channel.buffer(null);
    1260              : 
    1261          123 :         channel.addEventListener("close", function(event, options) {
    1262          123 :             const data = buffer.squash();
    1263          123 :             spawn_debug("process closed:", JSON.stringify(options));
    1264          123 :             if (data)
    1265          123 :                 spawn_debug("process output:", data);
    1266          123 :             if (options.message !== undefined)
    1267          123 :                 spawn_debug("process error:", options.message);
    1268              : 
    1269          123 :             if (options.problem)
    1270           43 :                 dfd.reject(new ProcessError(options, name));
    1271          123 :             else if (options["exit-status"] || options["exit-signal"])
    1272           56 :                 dfd.reject(new ProcessError(options, name), data);
    1273          123 :             else if (options.message !== undefined)
    1274           20 :                 dfd.resolve(data, options.message);
    1275              :             else
    1276           20 :                 dfd.resolve(data);
    1277          123 :         });
    1278              : 
    1279          123 :         const ret = dfd.promise;
    1280           48 :         ret.stream = function(callback) {
    1281           48 :             buffer.callback = callback.bind(ret);
    1282           48 :             return this;
    1283           48 :         };
    1284              : 
    1285           12 :         ret.input = function(message, stream) {
    1286           12 :             if (message !== null && message !== undefined) {
    1287           12 :                 spawn_debug("process input:", message);
    1288           12 :                 iterate_data(message, function(data) {
    1289           12 :                     channel.send(data);
    1290           12 :                 });
    1291           12 :             }
    1292           12 :             if (!stream)
    1293           10 :                 channel.control({ command: "done" });
    1294           12 :             return this;
    1295           12 :         };
    1296              : 
    1297           23 :         ret.close = function(problem) {
    1298           23 :             spawn_debug("process closing:", problem);
    1299           23 :             if (channel.valid)
    1300           23 :                 channel.close(problem);
    1301           23 :             return this;
    1302           23 :         };
    1303              : 
    1304          123 :         return ret;
    1305          123 :     };
    1306              : 
    1307              :     /* public */
    1308           51 :     cockpit.script = function(script, args, options) {
    1309           14 :         if (!options && is_plain_object(args)) {
    1310           14 :             options = args;
    1311           14 :             args = [];
    1312           14 :         }
    1313           51 :         const command = ["/bin/sh", "-c", script, "--"];
    1314           51 :         command.push.apply(command, args);
    1315           51 :         return cockpit.spawn(command, options);
    1316           51 :     };
    1317              : 
    1318          127 :     function dbus_debug() {
    1319           22 :         if (window.debugging == "all" || window.debugging?.includes("dbus"))
    1320           19 :             console.debug.apply(console, arguments);
    1321          127 :     }
    1322              : 
    1323          126 :     function DBusError(arg, arg1) {
    1324           48 :         if (typeof (arg) == "string") {
    1325           48 :             this.problem = arg;
    1326           48 :             this.name = null;
    1327           48 :             this.message = arg1 || cockpit.message(arg);
    1328           42 :         } else {
    1329          120 :             this.problem = null;
    1330          120 :             this.name = arg[0];
    1331           19 :             this.message = arg[1][0] || arg[0];
    1332          120 :         }
    1333            3 :         this.toString = function() {
    1334            3 :             return this.message;
    1335            3 :         };
    1336          126 :     }
    1337              : 
    1338          126 :     function DBusCache() {
    1339          126 :         const self = this;
    1340              : 
    1341          126 :         let callbacks = [];
    1342          126 :         self.data = { };
    1343          126 :         self.meta = { };
    1344              : 
    1345          126 :         self.connect = function connect(path, iface, callback, first) {
    1346          126 :             const cb = [path, iface, callback];
    1347          126 :             if (first)
    1348           49 :                 callbacks.unshift(cb);
    1349              :             else
    1350           49 :                 callbacks.push(cb);
    1351          126 :             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          126 :             };
    1363          126 :         };
    1364              : 
    1365          123 :         function emit(path, iface, props) {
    1366          123 :             const copy = callbacks.slice();
    1367          123 :             const length = copy.length;
    1368          123 :             for (let i = 0; i < length; i++) {
    1369          123 :                 const cb = copy[i];
    1370          123 :                 if ((!cb[0] || cb[0] === path) &&
    1371          123 :                     (!cb[1] || cb[1] === iface)) {
    1372          123 :                     cb[2](props, path);
    1373          123 :                 }
    1374          123 :             }
    1375          123 :         }
    1376              : 
    1377          123 :         self.update = function update(path, iface, props) {
    1378          123 :             if (!self.data[path])
    1379          123 :                 self.data[path] = { };
    1380          123 :             if (!self.data[path][iface])
    1381           62 :                 self.data[path][iface] = props;
    1382              :             else
    1383           62 :                 props = Object.assign(self.data[path][iface], props);
    1384          123 :             emit(path, iface, props);
    1385          123 :         };
    1386              : 
    1387            0 :         self.remove = function remove(path, iface) {
    1388            0 :             if (self.data[path]) {
    1389            0 :                 delete self.data[path][iface];
    1390            0 :                 emit(path, iface, null);
    1391            0 :             }
    1392            0 :         };
    1393              : 
    1394          126 :         self.lookup = function lookup(path, iface) {
    1395          126 :             if (self.data[path])
    1396           49 :                 return self.data[path][iface];
    1397          126 :             return undefined;
    1398          126 :         };
    1399              : 
    1400           36 :         self.each = function each(iface, callback) {
    1401            6 :             for (const path in self.data) {
    1402            6 :                 for (const ifa in self.data[path]) {
    1403            6 :                     if (ifa == iface)
    1404            6 :                         callback(self.data[path][iface], path);
    1405            6 :                 }
    1406            6 :             }
    1407           36 :         };
    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          126 :     }
    1418              : 
    1419          126 :     function DBusProxy(client, cache, iface, path, options) {
    1420          126 :         const self = this;
    1421          126 :         event_mixin(self, { });
    1422              : 
    1423          126 :         let valid = false;
    1424          126 :         let defined = false;
    1425          126 :         const waits = cockpit.defer();
    1426              : 
    1427              :         /* No enumeration on these properties */
    1428          126 :         Object.defineProperties(self, {
    1429          126 :             client: { value: client, enumerable: false, writable: false },
    1430          126 :             path: { value: path, enumerable: false, writable: false },
    1431          126 :             iface: { value: iface, enumerable: false, writable: false },
    1432          123 :             valid: { get: function() { return valid }, enumerable: false },
    1433          126 :             wait: {
    1434          126 :                 enumerable: false,
    1435          126 :                 writable: false,
    1436          123 :                 value: function(func) {
    1437          123 :                     if (func)
    1438          123 :                         waits.promise.always(func);
    1439          123 :                     return waits.promise;
    1440          123 :                 }
    1441          126 :             },
    1442          126 :             call: {
    1443            5 :                 value: function(name, args, options) { return client.call(path, iface, name, args, options) },
    1444          126 :                 enumerable: false,
    1445          126 :                 writable: false
    1446          126 :             },
    1447          126 :             data: { value: { }, enumerable: false }
    1448          126 :         });
    1449              : 
    1450          126 :         if (!options)
    1451           19 :             options = { };
    1452              : 
    1453          123 :         function define() {
    1454          123 :             if (!cache.meta[iface])
    1455          123 :                 return;
    1456              : 
    1457          123 :             const meta = cache.meta[iface];
    1458          123 :             defined = true;
    1459              : 
    1460           19 :             Object.keys(meta.methods || { }).forEach(function(name) {
    1461          123 :                 if (name[0].toLowerCase() == name[0])
    1462          123 :                     return; /* Only map upper case */
    1463              : 
    1464              :                 /* Again, make sure these don't show up in enumerations */
    1465          123 :                 Object.defineProperty(self, name, {
    1466          123 :                     enumerable: false,
    1467           49 :                     value: function() {
    1468           49 :                         const dfd = cockpit.defer();
    1469           49 :                         client.call(path, iface, name, Array.prototype.slice.call(arguments))
    1470           48 :                             .done(function(reply) { dfd.resolve.apply(dfd, reply) })
    1471            5 :                             .fail(function(ex) { dfd.reject(ex) });
    1472           49 :                         return dfd.promise;
    1473           49 :                     }
    1474          123 :                 });
    1475          123 :             });
    1476              : 
    1477           19 :             Object.keys(meta.properties || { }).forEach(function(name) {
    1478          123 :                 if (name[0].toLowerCase() == name[0])
    1479          123 :                     return; /* Only map upper case */
    1480              : 
    1481          123 :                 const config = {
    1482          123 :                     enumerable: true,
    1483          123 :                     get: function() { return self.data[name] },
    1484            0 :                     set: function(v) { throw Error(name + "is not writable") }
    1485          123 :                 };
    1486              : 
    1487          123 :                 const prop = meta.properties[name];
    1488           19 :                 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           19 :                 }
    1498              : 
    1499              :                 /* Again, make sure these don't show up in enumerations */
    1500          123 :                 Object.defineProperty(self, name, config);
    1501          123 :             });
    1502          123 :         }
    1503              : 
    1504          126 :         function update(props) {
    1505          123 :             if (props) {
    1506          123 :                 Object.assign(self.data, props);
    1507          123 :                 if (!defined)
    1508          123 :                     define();
    1509          123 :                 valid = true;
    1510          123 :             } else {
    1511          126 :                 valid = false;
    1512          126 :             }
    1513          126 :             self.dispatchEvent("changed", props);
    1514          126 :         }
    1515              : 
    1516          126 :         cache.connect(path, iface, update, true);
    1517          126 :         update(cache.lookup(path, iface));
    1518              : 
    1519           47 :         function signal(path, iface, name, args) {
    1520           47 :             self.dispatchEvent("signal", name, args);
    1521           47 :             if (name[0].toLowerCase() != name[0]) {
    1522           47 :                 args = args.slice();
    1523           47 :                 args.unshift(name);
    1524           47 :                 self.dispatchEvent.apply(self, args);
    1525           47 :             }
    1526           47 :         }
    1527              : 
    1528          126 :         client.subscribe({ path, interface: iface }, signal, options.subscribe !== false);
    1529              : 
    1530          126 :         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          126 :             if (valid)
    1538           41 :                 waits.resolve();
    1539           44 :             else if (ex instanceof DBusError)
    1540           19 :                 waits.reject(ex);
    1541              :             else
    1542           41 :                 waits.reject(new DBusError("not-found"));
    1543          126 :         }
    1544              : 
    1545              :         /* If watching then do a proper watch, otherwise object is done */
    1546          126 :         if (options.watch !== false)
    1547           42 :             client.watch({ path, interface: iface }).always(waited);
    1548              :         else
    1549           42 :             waited();
    1550          126 :     }
    1551              : 
    1552           36 :     function DBusProxies(client, cache, iface, path_namespace, options) {
    1553           36 :         const self = this;
    1554           36 :         event_mixin(self, { });
    1555              : 
    1556           36 :         self.client = client;
    1557           36 :         self.iface = iface;
    1558           36 :         self.path_namespace = path_namespace;
    1559              : 
    1560           36 :         let waits;
    1561              : 
    1562           36 :         self.wait = function(func) {
    1563           36 :             if (func)
    1564            6 :                 waits.always(func);
    1565           36 :             return waits;
    1566           36 :         };
    1567              : 
    1568           36 :         Object.defineProperties(self, {
    1569           36 :             client: { enumerable: false, writable: false },
    1570           36 :             iface: { enumerable: false, writable: false },
    1571           36 :             path_namespace: { enumerable: false, writable: false },
    1572           36 :             wait: { enumerable: false, writable: false },
    1573           36 :         });
    1574              : 
    1575              :         /* Subscribe to signals once for all proxies */
    1576           36 :         const match = { interface: iface, path_namespace };
    1577              : 
    1578              :         /* Callbacks added by proxies */
    1579           36 :         client.subscribe(match);
    1580              : 
    1581              :         /* Watch for property changes */
    1582           36 :         if (options.watch !== false) {
    1583           36 :             waits = client.watch(match);
    1584            6 :         } else {
    1585            6 :             waits = cockpit.defer().resolve().promise;
    1586            6 :         }
    1587              : 
    1588              :         /* Already added watch/subscribe, tell proxies not to */
    1589           36 :         options = { watch: false, subscribe: false, ...options };
    1590              : 
    1591           29 :         function update(props, path) {
    1592           29 :             let proxy = self[path];
    1593           29 :             if (path) {
    1594            6 :                 if (!props && proxy) {
    1595            6 :                     delete self[path];
    1596            6 :                     self.dispatchEvent("removed", proxy);
    1597            6 :                 } else if (props) {
    1598           29 :                     if (!proxy) {
    1599           29 :                         proxy = self[path] = client.proxy(iface, path, options);
    1600           29 :                         self.dispatchEvent("added", proxy);
    1601           29 :                     }
    1602           29 :                     self.dispatchEvent("changed", proxy);
    1603           29 :                 }
    1604           29 :             }
    1605           29 :         }
    1606              : 
    1607           36 :         cache.connect(null, iface, update, false);
    1608           36 :         cache.each(iface, update);
    1609           36 :     }
    1610              : 
    1611          127 :     function DBusClient(name, options) {
    1612          127 :         const self = this;
    1613          127 :         event_mixin(self, { });
    1614              : 
    1615          127 :         const args = { };
    1616          127 :         let track = false;
    1617          127 :         let owner = null;
    1618              : 
    1619          127 :         if (options) {
    1620          127 :             if (options.track)
    1621           29 :                 track = true;
    1622              : 
    1623          127 :             delete options.track;
    1624          127 :             Object.assign(args, options);
    1625          127 :         }
    1626          127 :         args.payload = "dbus-json3";
    1627          127 :         if (name)
    1628          123 :             args.name = name;
    1629          127 :         self.options = options;
    1630          127 :         self.unique_name = null;
    1631              : 
    1632          127 :         dbus_debug("dbus open: ", args);
    1633              : 
    1634          127 :         let channel = cockpit.channel(args);
    1635          127 :         const subscribers = { };
    1636          127 :         let calls = { };
    1637          127 :         let cache;
    1638              : 
    1639              :         /* The problem we closed with */
    1640          127 :         let closed;
    1641              : 
    1642          127 :         self.constructors = { "*": DBusProxy };
    1643              : 
    1644              :         /* Allows waiting on the channel if necessary */
    1645          127 :         self.wait = channel.wait;
    1646              : 
    1647          126 :         function ensure_cache() {
    1648          126 :             if (!cache)
    1649          126 :                 cache = new DBusCache();
    1650          126 :         }
    1651              : 
    1652          127 :         function send(payload) {
    1653          127 :             if (channel?.valid) {
    1654          127 :                 dbus_debug("dbus:", payload);
    1655          127 :                 channel.send(payload);
    1656          127 :                 return true;
    1657          127 :             }
    1658           19 :             return false;
    1659          127 :         }
    1660              : 
    1661           82 :         function matches(signal, match) {
    1662           75 :             if (match.path && signal[0] !== match.path)
    1663           64 :                 return false;
    1664           14 :             if (match.path_namespace && signal[0].indexOf(match.path_namespace) !== 0)
    1665           14 :                 return false;
    1666           80 :             if (match.interface && signal[1] !== match.interface)
    1667           44 :                 return false;
    1668           49 :             if (match.member && signal[2] !== match.member)
    1669           40 :                 return false;
    1670           14 :             if (match.arg0 && (!signal[3] || signal[3][0] !== match.arg0))
    1671           14 :                 return false;
    1672           82 :             return true;
    1673           82 :         }
    1674              : 
    1675          124 :         function on_message(event, payload) {
    1676          124 :             dbus_debug("dbus:", payload);
    1677          124 :             let msg;
    1678          124 :             try {
    1679          124 :                 msg = JSON.parse(payload);
    1680           19 :             } catch (ex) {
    1681           19 :                 console.warn("received invalid dbus json message:", ex);
    1682           19 :             }
    1683           19 :             if (msg === undefined) {
    1684           19 :                 channel.close({ problem: "protocol-error" });
    1685           19 :                 return;
    1686           19 :             }
    1687          123 :             const dfd = (msg.id !== undefined) ? calls[msg.id] : undefined;
    1688          124 :             if (msg.reply) {
    1689          124 :                 if (dfd) {
    1690          124 :                     const options = { };
    1691          124 :                     if (msg.type)
    1692          124 :                         options.type = msg.type;
    1693          124 :                     if (msg.flags)
    1694           32 :                         options.flags = msg.flags;
    1695          123 :                     dfd.resolve(msg.reply[0] || [], options);
    1696          124 :                     delete calls[msg.id];
    1697          124 :                 }
    1698          124 :                 return;
    1699          120 :             } else if (msg.error) {
    1700          120 :                 if (dfd) {
    1701          120 :                     dfd.reject(new DBusError(msg.error));
    1702          120 :                     delete calls[msg.id];
    1703          120 :                 }
    1704          120 :                 return;
    1705          120 :             }
    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          123 :             later_invoke(function() {
    1713           87 :                 if (msg.signal) {
    1714           87 :                     for (const id in subscribers) {
    1715           87 :                         const subscription = subscribers[id];
    1716           87 :                         if (subscription.callback) {
    1717           87 :                             if (matches(msg.signal, subscription.match))
    1718           87 :                                 subscription.callback.apply(self, msg.signal);
    1719           87 :                         }
    1720           87 :                     }
    1721           87 :                 } else if (msg.notify) {
    1722          123 :                     notify(msg.notify);
    1723          123 :                 } else if (msg.meta) {
    1724          123 :                     meta(msg.meta);
    1725          123 :                 } else if (msg.owner !== undefined) {
    1726          123 :                     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           29 :                     if (track && owner)
    1734           19 :                         self.close();
    1735              : 
    1736          123 :                     owner = msg.owner;
    1737           19 :                 } else {
    1738           19 :                     dbus_debug("received unexpected dbus json message:", payload);
    1739           19 :                 }
    1740          123 :             });
    1741          124 :         }
    1742              : 
    1743          123 :         function meta(data) {
    1744          123 :             ensure_cache();
    1745          123 :             Object.assign(cache.meta, data);
    1746          123 :             self.dispatchEvent("meta", data);
    1747          123 :         }
    1748              : 
    1749          123 :         function notify(data) {
    1750          123 :             ensure_cache();
    1751          123 :             for (const path in data) {
    1752          123 :                 for (const iface in data[path]) {
    1753          123 :                     const props = data[path][iface];
    1754          123 :                     if (!props)
    1755           19 :                         cache.remove(path, iface);
    1756              :                     else
    1757          123 :                         cache.update(path, iface, props);
    1758          123 :                 }
    1759          123 :             }
    1760          123 :             self.dispatchEvent("notify", data);
    1761          123 :         }
    1762              : 
    1763          127 :         this.notify = notify;
    1764              : 
    1765           46 :         function close_perform(options) {
    1766           41 :             closed = options.problem || "disconnected";
    1767           46 :             const outstanding = calls;
    1768           46 :             calls = { };
    1769           13 :             for (const id in outstanding) {
    1770           13 :                 outstanding[id].reject(new DBusError(closed, options.message));
    1771           13 :             }
    1772           46 :             self.dispatchEvent("close", options);
    1773           46 :         }
    1774              : 
    1775           41 :         this.close = function close(options) {
    1776           41 :             if (typeof options == "string")
    1777            4 :                 options = { problem: options };
    1778           41 :             if (!options)
    1779           41 :                 options = { };
    1780           41 :             if (channel)
    1781           10 :                 channel.close(options);
    1782              :             else
    1783           10 :                 close_perform(options);
    1784           41 :         };
    1785              : 
    1786          124 :         function on_ready(event, message) {
    1787          124 :             dbus_debug("dbus ready:", options);
    1788          124 :             self.unique_name = message["unique-name"];
    1789          124 :         }
    1790              : 
    1791           46 :         function on_close(event, options) {
    1792           46 :             dbus_debug("dbus close:", options);
    1793           46 :             channel.removeEventListener("ready", on_ready);
    1794           46 :             channel.removeEventListener("message", on_message);
    1795           46 :             channel.removeEventListener("close", on_close);
    1796           46 :             channel = null;
    1797           46 :             close_perform(options);
    1798           46 :         }
    1799              : 
    1800          127 :         channel.addEventListener("ready", on_ready);
    1801          127 :         channel.addEventListener("message", on_message);
    1802          127 :         channel.addEventListener("close", on_close);
    1803              : 
    1804          127 :         let last_cookie = 1;
    1805              : 
    1806          127 :         this.call = function call(path, iface, method, args, options) {
    1807          127 :             const dfd = cockpit.defer();
    1808          127 :             const id = String(last_cookie);
    1809          127 :             last_cookie++;
    1810          127 :             const method_call = {
    1811          127 :                 ...options,
    1812           62 :                 call: [path, iface, method, args || []],
    1813          127 :                 id
    1814          127 :             };
    1815              : 
    1816          127 :             const msg = JSON.stringify(method_call);
    1817          127 :             if (send(msg))
    1818           19 :                 calls[id] = dfd;
    1819              :             else
    1820           19 :                 dfd.reject(new DBusError(closed));
    1821              : 
    1822          127 :             return dfd.promise;
    1823          127 :         };
    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          126 :         this.subscribe = function subscribe(match, callback, rule) {
    1835          126 :             const subscription = {
    1836          126 :                 match: { ...match },
    1837          126 :                 callback
    1838          126 :             };
    1839              : 
    1840          126 :             if (rule !== false)
    1841          126 :                 send(JSON.stringify({ "add-match": subscription.match }));
    1842              : 
    1843          126 :             let id;
    1844          126 :             if (callback) {
    1845          126 :                 id = String(last_cookie);
    1846          126 :                 last_cookie++;
    1847          126 :                 subscribers[id] = subscription;
    1848          126 :             }
    1849              : 
    1850          126 :             return {
    1851           22 :                 remove: function() {
    1852           22 :                     let prev;
    1853           22 :                     if (id) {
    1854           22 :                         prev = subscribers[id];
    1855           22 :                         if (prev)
    1856           22 :                             delete subscribers[id];
    1857           22 :                     }
    1858           22 :                     if (rule !== false && prev)
    1859           22 :                         send(JSON.stringify({ "remove-match": prev.match }));
    1860           22 :                 }
    1861          126 :             };
    1862          126 :         };
    1863              : 
    1864          126 :         self.watch = function watch(path) {
    1865           47 :             const match = is_plain_object(path) ? { ...path } : { path: String(path) };
    1866              : 
    1867          126 :             const id = String(last_cookie);
    1868          126 :             last_cookie++;
    1869          126 :             const dfd = cockpit.defer();
    1870              : 
    1871          126 :             const msg = JSON.stringify({ watch: match, id });
    1872          126 :             if (send(msg))
    1873           19 :                 calls[id] = dfd;
    1874              :             else
    1875           19 :                 dfd.reject(new DBusError(closed));
    1876              : 
    1877          126 :             const ret = dfd.promise;
    1878            1 :             ret.remove = function remove() {
    1879            1 :                 if (id in calls) {
    1880            1 :                     dfd.reject(new DBusError("cancelled"));
    1881            1 :                     delete calls[id];
    1882            1 :                 }
    1883            1 :                 send(JSON.stringify({ unwatch: match }));
    1884            1 :             };
    1885          126 :             return ret;
    1886          126 :         };
    1887              : 
    1888          126 :         self.proxy = function proxy(iface, path, options) {
    1889          126 :             if (!iface)
    1890          123 :                 iface = name;
    1891          126 :             iface = String(iface);
    1892          126 :             if (!path)
    1893          123 :                 path = "/" + iface.replaceAll(".", "/");
    1894          126 :             let Constructor = self.constructors[iface];
    1895          126 :             if (!Constructor)
    1896          126 :                 Constructor = self.constructors["*"];
    1897          126 :             if (!options)
    1898          126 :                 options = { };
    1899          126 :             ensure_cache();
    1900          126 :             return new Constructor(self, cache, iface, String(path), options);
    1901          126 :         };
    1902              : 
    1903           36 :         self.proxies = function proxies(iface, path_namespace, options) {
    1904           36 :             if (!iface)
    1905            6 :                 iface = name;
    1906           36 :             if (!path_namespace)
    1907            6 :                 path_namespace = "/";
    1908           36 :             if (!options)
    1909           36 :                 options = { };
    1910           36 :             ensure_cache();
    1911           36 :             return new DBusProxies(self, cache, String(iface), String(path_namespace), options);
    1912           36 :         };
    1913          127 :     }
    1914              : 
    1915              :     /* Well known buses */
    1916          127 :     const shared_dbus = {
    1917          127 :         internal: null,
    1918          127 :         session: null,
    1919          127 :         system: null,
    1920          127 :     };
    1921              : 
    1922              :     /* public */
    1923          127 :     cockpit.dbus = function dbus(name, options) {
    1924          127 :         if (!options)
    1925           93 :             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          127 :         const keys = Object.keys(options);
    1934          127 :         const bus = options.bus;
    1935          127 :         const shared = !name && keys.length == 1 && bus in shared_dbus;
    1936              : 
    1937          127 :         if (shared && shared_dbus[bus])
    1938          126 :             return shared_dbus[bus];
    1939              : 
    1940          127 :         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          127 :         if (shared) {
    1948          127 :             const old_close = client.close;
    1949          127 :             client.close = function() {
    1950          127 :                 if (arguments.length > 0)
    1951           19 :                     old_close.apply(client, arguments);
    1952          127 :             };
    1953            5 :             client.addEventListener("close", function() {
    1954            5 :                 if (shared_dbus[bus] == client)
    1955            5 :                     shared_dbus[bus] = null;
    1956            5 :             });
    1957          127 :             shared_dbus[bus] = client;
    1958          127 :         }
    1959              : 
    1960          127 :         return client;
    1961          127 :     };
    1962              : 
    1963            0 :     cockpit.variant = function variant(type, value) {
    1964            0 :         return { v: value, t: type };
    1965            0 :     };
    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          123 :     cockpit.file = function file(path, options) {
    1976           89 :         options = options || { };
    1977          123 :         const binary = options.binary;
    1978              : 
    1979          123 :         const self = {
    1980          123 :             path,
    1981          123 :             read,
    1982          123 :             replace,
    1983          123 :             modify,
    1984              : 
    1985          123 :             watch,
    1986              : 
    1987          123 :             close
    1988          123 :         };
    1989              : 
    1990          123 :         const base_channel_options = { ...options };
    1991          123 :         delete base_channel_options.syntax;
    1992              : 
    1993          123 :         function parse(str) {
    1994          119 :             if (options.syntax?.parse)
    1995           83 :                 return options.syntax.parse(str);
    1996              :             else
    1997           87 :                 return str;
    1998          123 :         }
    1999              : 
    2000            6 :         function stringify(obj) {
    2001            0 :             if (options.syntax?.stringify)
    2002            0 :                 return options.syntax.stringify(obj);
    2003              :             else
    2004            6 :                 return obj;
    2005            6 :         }
    2006              : 
    2007          123 :         let read_promise = null;
    2008          123 :         let read_channel;
    2009              : 
    2010          123 :         function read() {
    2011          123 :             if (read_promise)
    2012           19 :                 return read_promise;
    2013              : 
    2014          123 :             const dfd = cockpit.defer();
    2015          123 :             const opts = {
    2016          123 :                 ...base_channel_options,
    2017          123 :                 payload: "fsread1",
    2018          123 :                 path
    2019          123 :             };
    2020              : 
    2021          123 :             function try_read() {
    2022          123 :                 read_channel = cockpit.channel(opts);
    2023          123 :                 const content_parts = [];
    2024          123 :                 read_channel.addEventListener("message", function (event, message) {
    2025          123 :                     content_parts.push(message);
    2026          123 :                 });
    2027          123 :                 read_channel.addEventListener("close", function (event, message) {
    2028          123 :                     read_channel = null;
    2029              : 
    2030           19 :                     if (message.problem == "change-conflict") {
    2031           19 :                         try_read();
    2032           19 :                         return;
    2033           19 :                     }
    2034              : 
    2035          123 :                     read_promise = null;
    2036              : 
    2037           19 :                     if (message.problem) {
    2038           19 :                         const error = new BasicError(message.problem, message.message);
    2039           19 :                         fire_watch_callbacks(null, null, error);
    2040           19 :                         dfd.reject(error);
    2041           19 :                         return;
    2042           19 :                     }
    2043              : 
    2044          123 :                     let content;
    2045          123 :                     if (message.tag == "-")
    2046           28 :                         content = null;
    2047          123 :                     else {
    2048          123 :                         try {
    2049          123 :                             content = parse(join_data(content_parts, binary));
    2050           19 :                         } catch (e) {
    2051           19 :                             fire_watch_callbacks(null, null, e);
    2052           19 :                             dfd.reject(e);
    2053           19 :                             return;
    2054           19 :                         }
    2055          123 :                     }
    2056              : 
    2057          123 :                     fire_watch_callbacks(content, message.tag);
    2058          123 :                     dfd.resolve(content, message.tag);
    2059          123 :                 });
    2060          123 :             }
    2061              : 
    2062          123 :             try_read();
    2063              : 
    2064          123 :             read_promise = dfd.promise;
    2065          123 :             return read_promise;
    2066          123 :         }
    2067              : 
    2068          123 :         let replace_channel = null;
    2069              : 
    2070           10 :         function replace(new_content, expected_tag) {
    2071           10 :             const dfd = cockpit.defer();
    2072              : 
    2073           10 :             let file_content;
    2074           10 :             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           10 :             if (replace_channel)
    2082            0 :                 replace_channel.close("abort");
    2083              : 
    2084           10 :             const opts = {
    2085           10 :                 ...base_channel_options,
    2086           10 :                 payload: "fsreplace1",
    2087           10 :                 path,
    2088           10 :                 tag: expected_tag
    2089           10 :             };
    2090           10 :             replace_channel = cockpit.channel(opts);
    2091              : 
    2092           10 :             replace_channel.addEventListener("close", function (event, message) {
    2093           10 :                 replace_channel = null;
    2094            1 :                 if (message.problem) {
    2095            1 :                     dfd.reject(new BasicError(message.problem, message.message));
    2096            1 :                 } else {
    2097           10 :                     fire_watch_callbacks(new_content, message.tag);
    2098           10 :                     dfd.resolve(message.tag);
    2099           10 :                 }
    2100           10 :             });
    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            6 :             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            6 :                     iterate_data(file_content, data => {
    2112            6 :                         replace_channel.send(data);
    2113            6 :                     });
    2114            6 :                 }
    2115            6 :             }
    2116              : 
    2117           10 :             replace_channel.control({ command: "done" });
    2118           10 :             return dfd.promise;
    2119           10 :         }
    2120              : 
    2121            2 :         function modify(callback, initial_content, initial_tag) {
    2122            2 :             const dfd = cockpit.defer();
    2123              : 
    2124            2 :             function update(content, tag) {
    2125            2 :                 let new_content = callback(content);
    2126            2 :                 if (new_content === undefined)
    2127            0 :                     new_content = content;
    2128            2 :                 replace(new_content, tag)
    2129            2 :                     .done(function (new_tag) {
    2130            2 :                         dfd.resolve(new_content, new_tag);
    2131            2 :                     })
    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            2 :             }
    2139              : 
    2140            2 :             function read_then_update() {
    2141            2 :                 read()
    2142            2 :                     .done(update)
    2143            0 :                     .fail(function (error) {
    2144            0 :                         dfd.reject(error);
    2145            0 :                     });
    2146            2 :             }
    2147              : 
    2148            2 :             if (initial_content === undefined)
    2149            0 :                 read_then_update();
    2150              :             else
    2151            0 :                 update(initial_content, initial_tag);
    2152              : 
    2153            2 :             return dfd.promise;
    2154            2 :         }
    2155              : 
    2156          123 :         const watch_callbacks = [];
    2157          123 :         let n_watch_callbacks = 0;
    2158              : 
    2159          123 :         let watch_channel = null;
    2160          123 :         let watch_tag;
    2161              : 
    2162           54 :         function ensure_watch_channel(options) {
    2163           54 :             if (n_watch_callbacks > 0) {
    2164           54 :                 if (watch_channel)
    2165           54 :                     return;
    2166              : 
    2167           54 :                 watch_channel = new FsInfoClient(path, ["tag"], { superuser: base_channel_options.superuser });
    2168           54 :                 watch_channel.on('change', (state) => {
    2169           46 :                     if (state.error) {
    2170              :                         // Behave like fsread1, not-found is not a fatal error
    2171           46 :                         if (state.error.problem === "not-found") {
    2172           46 :                             fire_watch_callbacks(null, "-");
    2173           12 :                         } else {
    2174           12 :                             const error = new BasicError(state.error.problem, state.error.message);
    2175           12 :                             fire_watch_callbacks(null, null, error);
    2176           12 :                         }
    2177           44 :                     } else if (state.info && state.info.tag) {
    2178              :                         // otherwise, the file is present with the given tag
    2179           52 :                         if (state.info.tag !== watch_tag) {
    2180              :                             // cockpit.file.watch() defaults to reading
    2181           20 :                             if (options?.read === false)
    2182           20 :                                 fire_watch_callbacks(null, state.info.tag);
    2183              :                             else
    2184           52 :                                 read();
    2185           52 :                         }
    2186           52 :                     }
    2187           54 :                 });
    2188              : 
    2189              :                 // fallback when running against bridge < 310
    2190            3 :                 watch_channel.on('close', ex => {
    2191            1 :                     if (ex.problem === 'not-supported') {
    2192            1 :                         const opts = {
    2193            1 :                             payload: "fswatch1",
    2194            1 :                             path,
    2195            1 :                             superuser: base_channel_options.superuser,
    2196            1 :                         };
    2197            1 :                         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            1 :                         read();
    2214            1 :                     }
    2215            3 :                 });
    2216           12 :             } else {
    2217           12 :                 if (watch_channel) {
    2218           12 :                     watch_channel.close();
    2219           12 :                     watch_channel = null;
    2220           12 :                 }
    2221           12 :             }
    2222           54 :         }
    2223              : 
    2224          123 :         function fire_watch_callbacks(/* content, tag, error */) {
    2225           21 :             watch_tag = arguments[1] || null;
    2226          123 :             invoke_functions(watch_callbacks, self, arguments);
    2227          123 :         }
    2228              : 
    2229           54 :         function watch(callback, options) {
    2230           54 :             if (callback)
    2231           54 :                 watch_callbacks.push(callback);
    2232           54 :             n_watch_callbacks += 1;
    2233           54 :             ensure_watch_channel(options);
    2234              : 
    2235           54 :             watch_tag = null;
    2236              : 
    2237           54 :             return {
    2238            3 :                 remove: function () {
    2239            3 :                     if (callback) {
    2240            3 :                         const index = watch_callbacks.indexOf(callback);
    2241            3 :                         if (index > -1)
    2242            3 :                             watch_callbacks[index] = null;
    2243            3 :                     }
    2244            3 :                     n_watch_callbacks -= 1;
    2245            3 :                     ensure_watch_channel(options);
    2246            3 :                 }
    2247           54 :             };
    2248           54 :         }
    2249              : 
    2250           38 :         function close() {
    2251           38 :             if (read_channel)
    2252            7 :                 read_channel.close("cancelled");
    2253           38 :             if (replace_channel)
    2254            7 :                 replace_channel.close("cancelled");
    2255           38 :             if (watch_channel)
    2256            7 :                 watch_channel.close();
    2257           38 :         }
    2258              : 
    2259          123 :         return self;
    2260          123 :     };
    2261              : 
    2262              :     /* ---------------------------------------------------------------------
    2263              :      * Localization
    2264              :      */
    2265              : 
    2266          127 :     let po_data = { };
    2267          127 :     let po_plural;
    2268              : 
    2269          127 :     cockpit.language = "en";
    2270          127 :     cockpit.language_direction = "ltr";
    2271          127 :     const test_l10n = window.localStorage.test_l10n;
    2272              : 
    2273            1 :     cockpit.locale = function locale(po) {
    2274            1 :         let lang = cockpit.language;
    2275            1 :         let lang_dir = cockpit.language_direction;
    2276            1 :         let header;
    2277              : 
    2278            1 :         if (po) {
    2279            1 :             Object.assign(po_data, po);
    2280            1 :             header = po[""];
    2281            1 :         } else if (po === null) {
    2282            1 :             po_data = { };
    2283            1 :         }
    2284              : 
    2285            1 :         if (header) {
    2286            1 :             if (header["plural-forms"])
    2287            1 :                 po_plural = header["plural-forms"];
    2288            1 :             if (header.language)
    2289            1 :                 lang = header.language;
    2290            1 :             if (header["language-direction"])
    2291            1 :                 lang_dir = header["language-direction"];
    2292            1 :         }
    2293              : 
    2294            1 :         cockpit.language = lang;
    2295            1 :         cockpit.language_direction = lang_dir;
    2296            1 :     };
    2297              : 
    2298          127 :     cockpit.translate = function translate(/* ... */) {
    2299          127 :         let what;
    2300              : 
    2301              :         /* Called without arguments, entire document */
    2302          127 :         if (arguments.length === 0)
    2303           19 :             what = [document];
    2304              : 
    2305              :         /* Called with a single array like argument */
    2306           19 :         else if (arguments.length === 1 && arguments[0].length)
    2307           19 :             what = arguments[0];
    2308              : 
    2309              :         /* Called with 1 or more element arguments */
    2310              :         else
    2311           19 :             what = arguments;
    2312              : 
    2313              :         /* Translate all the things */
    2314          127 :         const wlen = what.length;
    2315          127 :         for (let w = 0; w < wlen; w++) {
    2316              :             /* The list of things to translate */
    2317          127 :             let list = null;
    2318          127 :             if (what[w].querySelectorAll)
    2319          127 :                 list = what[w].querySelectorAll("[translate]");
    2320          127 :             if (!list)
    2321          127 :                 continue;
    2322              : 
    2323              :             /* Each element */
    2324           28 :             for (let i = 0; i < list.length; i++) {
    2325           28 :                 const el = list[i];
    2326              : 
    2327           19 :                 let val = el.getAttribute("translate") || "yes";
    2328           28 :                 if (val == "no")
    2329           28 :                     continue;
    2330              : 
    2331              :                 /* Each thing to translate */
    2332           28 :                 const tasks = val.split(" ");
    2333           28 :                 val = el.getAttribute("translate-context") || el.getAttribute("context");
    2334           28 :                 for (let t = 0; t < tasks.length; t++) {
    2335           19 :                     if (tasks[t] == "yes" || tasks[t] == "translate")
    2336           19 :                         el.textContent = cockpit.gettext(val, el.textContent);
    2337           19 :                     else if (tasks[t])
    2338           19 :                         el.setAttribute(tasks[t], cockpit.gettext(val, el.getAttribute(tasks[t]) || ""));
    2339           28 :                 }
    2340              : 
    2341              :                 /* Mark this thing as translated */
    2342           28 :                 el.removeAttribute("translate");
    2343           28 :             }
    2344          127 :         }
    2345          127 :     };
    2346              : 
    2347          138 :     cockpit.gettext = function gettext(context, string) {
    2348              :         /* Missing first parameter */
    2349          138 :         if (arguments.length == 1) {
    2350          138 :             string = context;
    2351          138 :             context = undefined;
    2352          138 :         }
    2353              : 
    2354           56 :         const key = context ? context + '\u0004' + string : string;
    2355          138 :         if (po_data) {
    2356          138 :             const translated = po_data[key];
    2357           30 :             if (translated?.[1])
    2358           30 :                 string = translated[1];
    2359          138 :         }
    2360              : 
    2361          138 :         if (test_l10n === 'true')
    2362           30 :             return "»" + string + "«";
    2363              : 
    2364          138 :         return string;
    2365          138 :     };
    2366              : 
    2367            0 :     function imply(val) {
    2368            0 :         return (val === true ? 1 : val || 0);
    2369            0 :     }
    2370              : 
    2371           53 :     cockpit.ngettext = function ngettext(context, string1, stringN, num) {
    2372              :         /* Missing first parameter */
    2373           53 :         if (arguments.length == 3) {
    2374           53 :             num = stringN;
    2375           53 :             stringN = string1;
    2376           53 :             string1 = context;
    2377           53 :             context = undefined;
    2378           53 :         }
    2379              : 
    2380            8 :         const key = context ? context + '\u0004' + string1 : string1;
    2381            8 :         if (po_data && po_plural) {
    2382            8 :             const translated = po_data[key];
    2383            8 :             if (translated) {
    2384            8 :                 const i = imply(po_plural(num)) + 1;
    2385            8 :                 if (translated[i])
    2386            8 :                     return translated[i];
    2387            8 :             }
    2388            8 :         }
    2389           53 :         if (num == 1)
    2390           53 :             return string1;
    2391           12 :         return stringN;
    2392           53 :     };
    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          127 :     const _ = cockpit.gettext;
    2400              : 
    2401           65 :     cockpit.message = function message(arg) {
    2402           65 :         if (arg.message)
    2403            9 :             return arg.message;
    2404              : 
    2405           64 :         let problem = null;
    2406           64 :         if (arg.problem)
    2407            8 :             problem = arg.problem;
    2408              :         else
    2409           63 :             problem = arg + "";
    2410           64 :         if (problem == "terminated")
    2411            8 :             return _("Your session has been terminated.");
    2412           63 :         else if (problem == "no-session")
    2413            8 :             return _("Your session has expired. Please log in again.");
    2414           63 :         else if (problem == "access-denied")
    2415            9 :             return _("Not permitted to perform this action.");
    2416           60 :         else if (problem == "authentication-failed")
    2417            9 :             return _("Login failed");
    2418           60 :         else if (problem == "authentication-not-supported")
    2419            8 :             return _("The server refused to authenticate using any supported methods.");
    2420           60 :         else if (problem == "unknown-hostkey")
    2421            8 :             return _("Untrusted host");
    2422           60 :         else if (problem == "unknown-host")
    2423            8 :             return _("Untrusted host");
    2424           60 :         else if (problem == "invalid-hostkey")
    2425            8 :             return _("Host key is incorrect");
    2426           60 :         else if (problem == "internal-error")
    2427            8 :             return _("Internal error");
    2428           60 :         else if (problem == "timeout")
    2429            8 :             return _("Connection has timed out.");
    2430           60 :         else if (problem == "no-cockpit")
    2431            8 :             return _("Cockpit is not installed on the system.");
    2432           59 :         else if (problem == "no-forwarding")
    2433            8 :             return _("Cannot forward login credentials");
    2434           59 :         else if (problem == "disconnected")
    2435            9 :             return _("Server has closed the connection.");
    2436           55 :         else if (problem == "not-supported")
    2437            8 :             return _("Cockpit is not compatible with the software on the system.");
    2438           55 :         else if (problem == "no-host")
    2439            8 :             return _("Cockpit could not contact the given host.");
    2440           55 :         else if (problem == "too-large")
    2441            8 :             return _("Too much data");
    2442              :         else
    2443           55 :             return problem;
    2444           65 :     };
    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            0 :     function check_superuser() {
    2705            0 :         return new Promise((resolve, reject) => {
    2706            0 :             const ch = cockpit.channel({ payload: "null", superuser: "require" });
    2707            0 :             ch.wait()
    2708            0 :                 .then(() => resolve(true))
    2709            0 :                 .catch(() => resolve(false))
    2710            0 :                 .always(() => ch.close());
    2711            0 :         });
    2712            0 :     }
    2713              : 
    2714            0 :     function Permission(options) {
    2715            0 :         const self = this;
    2716            0 :         event_mixin(self, { });
    2717              : 
    2718            0 :         const api = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
    2719            0 :         api.addEventListener("changed", maybe_reload);
    2720              : 
    2721            0 :         function maybe_reload() {
    2722            0 :             if (api.valid && self.allowed !== null) {
    2723            0 :                 if (self.allowed != (api.Current != "none"))
    2724            0 :                     window.location.reload(true);
    2725            0 :             }
    2726            0 :         }
    2727              : 
    2728            0 :         self.allowed = null;
    2729            0 :         self.user = options ? options.user : null; // pre-fill for unit tests
    2730            0 :         self.is_superuser = options ? options._is_superuser : null; // pre-fill for unit tests
    2731              : 
    2732            0 :         let group = null;
    2733            0 :         let admin = false;
    2734              : 
    2735            0 :         if (options)
    2736            0 :             group = options.group;
    2737              : 
    2738            0 :         if (options?.admin)
    2739            0 :             admin = true;
    2740              : 
    2741            0 :         function decide(user) {
    2742            0 :             if (user.id === 0)
    2743            0 :                 return true;
    2744              : 
    2745            0 :             if (group)
    2746            0 :                 return !!(user.groups || []).includes(group);
    2747              : 
    2748            0 :             if (admin)
    2749            0 :                 return self.is_superuser;
    2750              : 
    2751            0 :             if (user.id === undefined)
    2752            0 :                 return null;
    2753              : 
    2754            0 :             return false;
    2755            0 :         }
    2756              : 
    2757            0 :         if (self.user && self.is_superuser !== null) {
    2758            0 :             self.allowed = decide(self.user);
    2759            0 :         } else {
    2760            0 :             Promise.all([cockpit.user(), check_superuser()])
    2761            0 :                 .then(([user, is_superuser]) => {
    2762            0 :                     self.user = user;
    2763            0 :                     self.is_superuser = is_superuser;
    2764            0 :                     const allowed = decide(user);
    2765            0 :                     if (self.allowed !== allowed) {
    2766            0 :                         self.allowed = allowed;
    2767            0 :                         maybe_reload();
    2768            0 :                         self.dispatchEvent("changed");
    2769            0 :                     }
    2770            0 :                 });
    2771            0 :         }
    2772              : 
    2773            0 :         self.close = function close() {
    2774              :             /* no-op for now */
    2775            0 :         };
    2776            0 :     }
    2777              : 
    2778            0 :     cockpit.permission = function permission(arg) {
    2779            0 :         return new Permission(arg);
    2780            0 :     };
    2781              : 
    2782              :     /* ---------------------------------------------------------------------
    2783              :      * Metrics
    2784              :      *
    2785              :      */
    2786              : 
    2787            1 :     function MetricsChannel(interval, options_list, cache) {
    2788            1 :         const self = this;
    2789            1 :         event_mixin(self, { });
    2790              : 
    2791            1 :         if (options_list.length === undefined)
    2792            0 :             options_list = [options_list];
    2793              : 
    2794            1 :         const channels = [];
    2795            1 :         let following = false;
    2796              : 
    2797            1 :         self.series = cockpit.series(interval, cache, fetch_for_series);
    2798            1 :         self.archives = null;
    2799            1 :         self.meta = null;
    2800              : 
    2801            1 :         function fetch_for_series(beg, end, for_walking) {
    2802            1 :             if (!for_walking)
    2803            0 :                 self.fetch(beg, end);
    2804              :             else
    2805            0 :                 self.follow();
    2806            1 :         }
    2807              : 
    2808            1 :         function transfer(options_list, callback, is_archive) {
    2809            1 :             if (options_list.length === 0)
    2810            1 :                 return;
    2811              : 
    2812            1 :             if (!is_archive) {
    2813            1 :                 if (following)
    2814            1 :                     return;
    2815            1 :                 following = true;
    2816            1 :             }
    2817              : 
    2818            1 :             const options = {
    2819            1 :                 payload: "metrics1",
    2820            1 :                 interval,
    2821            1 :                 source: "internal",
    2822            1 :                 ...options_list[0]
    2823            1 :             };
    2824              : 
    2825            1 :             delete options.archive_source;
    2826              : 
    2827            1 :             const channel = cockpit.channel(options);
    2828            1 :             channels.push(channel);
    2829              : 
    2830            1 :             let meta = null;
    2831            1 :             let last = null;
    2832            1 :             let beg;
    2833              : 
    2834            1 :             channel.addEventListener("close", function(ev, close_options) {
    2835            1 :                 if (!is_archive)
    2836            0 :                     following = false;
    2837              : 
    2838            1 :                 if (options_list.length > 1 &&
    2839            0 :                     (close_options.problem == "not-supported" || close_options.problem == "not-found")) {
    2840            0 :                     transfer(options_list.slice(1), callback);
    2841            0 :                 } else if (close_options.problem) {
    2842            1 :                     if (close_options.problem != "terminated" &&
    2843            1 :                         close_options.problem != "disconnected" &&
    2844            1 :                         close_options.problem != "authentication-failed" &&
    2845            1 :                         (close_options.problem != "not-found" || !is_archive) &&
    2846            0 :                         (close_options.problem != "not-supported" || !is_archive)) {
    2847            0 :                         console.warn("metrics channel failed: " + close_options.problem);
    2848            0 :                     }
    2849            0 :                 } else if (is_archive) {
    2850            0 :                     if (!self.archives) {
    2851            0 :                         self.archives = true;
    2852            0 :                         self.dispatchEvent('changed');
    2853            0 :                     }
    2854            0 :                 }
    2855            1 :             });
    2856              : 
    2857            1 :             channel.addEventListener("message", function(ev, payload) {
    2858            1 :                 const message = JSON.parse(payload);
    2859              : 
    2860              :                 /* A meta message? */
    2861            1 :                 const message_len = message.length;
    2862            1 :                 if (message_len === undefined) {
    2863            1 :                     meta = message;
    2864            1 :                     let timestamp = 0;
    2865            1 :                     if (meta.now && meta.timestamp)
    2866            1 :                         timestamp = meta.timestamp + (Date.now() - meta.now);
    2867            1 :                     beg = Math.floor(timestamp / interval);
    2868            1 :                     callback(beg, meta, null, options_list[0]);
    2869              : 
    2870              :                     /* Trigger to outside interest that meta changed */
    2871            1 :                     self.meta = meta;
    2872            1 :                     self.dispatchEvent('changed');
    2873              : 
    2874              :                 /* A data message */
    2875            1 :                 } else if (meta) {
    2876              :                     /* Data decompression */
    2877            1 :                     for (let i = 0; i < message_len; i++) {
    2878            1 :                         const data = message[i];
    2879            1 :                         if (last) {
    2880            1 :                             for (let j = 0; j < last.length; j++) {
    2881            1 :                                 const dataj = data[j];
    2882            0 :                                 if (dataj === null || dataj === undefined) {
    2883            0 :                                     data[j] = last[j];
    2884            0 :                                 } else {
    2885            1 :                                     const dataj_len = dataj.length;
    2886            1 :                                     if (dataj_len !== undefined) {
    2887            1 :                                         const lastj = last[j];
    2888            1 :                                         const lastj_len = last[j].length;
    2889            1 :                                         let k;
    2890            1 :                                         for (k = 0; k < dataj_len; k++) {
    2891            1 :                                             if (dataj[k] === null)
    2892            0 :                                                 dataj[k] = lastj[k];
    2893            1 :                                         }
    2894            1 :                                         for (; k < lastj_len; k++)
    2895            0 :                                             dataj[k] = lastj[k];
    2896            1 :                                     }
    2897            1 :                                 }
    2898            1 :                             }
    2899            1 :                         }
    2900            1 :                         last = data;
    2901            1 :                     }
    2902              : 
    2903              :                     /* Return the data */
    2904            1 :                     callback(beg, meta, message, options_list[0]);
    2905              : 
    2906              :                     /* Bump timestamp for the next message */
    2907            1 :                     beg += message_len;
    2908            1 :                     meta.timestamp += (interval * message_len);
    2909            1 :                 }
    2910            1 :             });
    2911            1 :         }
    2912              : 
    2913            1 :         function drain(beg, meta, message, options) {
    2914              :             /* Generate a mapping object if necessary */
    2915            1 :             let mapping = meta.mapping;
    2916            1 :             if (!mapping) {
    2917            1 :                 mapping = { };
    2918            1 :                 meta.metrics.forEach(function(metric, i) {
    2919            1 :                     const map = { "": i };
    2920            0 :                     const name = options.metrics_path_names?.[i] ?? metric.name;
    2921            1 :                     mapping[name] = map;
    2922            1 :                     if (metric.instances) {
    2923            1 :                         metric.instances.forEach(function(instance, i) {
    2924            1 :                             if (instance === "")
    2925            0 :                                 instance = "/";
    2926            1 :                             map[instance] = { "": i };
    2927            1 :                         });
    2928            1 :                     }
    2929            1 :                 });
    2930            1 :                 meta.mapping = mapping;
    2931            1 :             }
    2932              : 
    2933            1 :             if (message)
    2934            1 :                 self.series.input(beg, message, mapping);
    2935            1 :         }
    2936              : 
    2937            1 :         self.fetch = function fetch(beg, end) {
    2938            1 :             const timestamp = beg * interval - Date.now();
    2939            1 :             const limit = end - beg;
    2940              : 
    2941            1 :             const archive_options_list = [];
    2942            1 :             for (let i = 0; i < options_list.length; i++) {
    2943            1 :                 if (options_list[i].archive_source) {
    2944            1 :                     archive_options_list.push({
    2945            1 :                                                    ...options_list[i],
    2946            1 :                                                    source: options_list[i].archive_source,
    2947            1 :                                                    timestamp,
    2948            1 :                                                    limit
    2949            1 :                                               });
    2950            1 :                 }
    2951            1 :             }
    2952              : 
    2953            1 :             transfer(archive_options_list, drain, true);
    2954            1 :         };
    2955              : 
    2956            1 :         self.follow = function follow() {
    2957            1 :             transfer(options_list, drain);
    2958            1 :         };
    2959              : 
    2960            0 :         self.close = function close(options) {
    2961            0 :             const len = channels.length;
    2962            0 :             if (self.series)
    2963            0 :                 self.series.close();
    2964              : 
    2965            0 :             for (let i = 0; i < len; i++)
    2966            0 :                 channels[i].close(options);
    2967            0 :         };
    2968            1 :     }
    2969              : 
    2970            1 :     cockpit.metrics = function metrics(interval, options) {
    2971            1 :         return new MetricsChannel(interval, options);
    2972            1 :     };
    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          127 :     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          123 :     cockpit.assert = (predicate, message) => {
    2999           19 :         if (!predicate) {
    3000           19 :             throw new Error(`Assertion failed: ${message}`);
    3001           19 :         }
    3002          123 :     };
    3003              : 
    3004          127 :     return cockpit;
    3005          127 : }
    3006              : 
    3007          127 : const cockpit = factory();
    3008              : 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          127 : window.cockpit = cockpit;
        

Generated by: LCOV version 2.0-1