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