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