LCOV - code coverage report
Current view: top level - pkg/shell/machines - machines.js Coverage Total Hit
Test: cockpit Lines: 74.0 % 589 436
Test Date: 2026-06-16 14:09:37

            Line data    Source code
       1              : // SPDX-License-Identifier: LGPL-2.1-or-later
       2          123 : import cockpit from "cockpit";
       3              : import { import_Manifests } from "../manifests";
       4              : import { validate } from "import-json";
       5              : import { host_superuser_storage_key } from "superuser-dialogs";
       6              : 
       7              : import ssh_add_key_sh from "../../lib/ssh-add-key.sh";
       8              : 
       9          123 : const mod = { };
      10              : 
      11              : /*
      12              :  * We share the Machines state between multiple frames. Only
      13              :  * one frame has the job of loading the state, usually index.js
      14              :  * The Loader code below does all the loading.
      15              :  *
      16              :  * The data is stored in sessionStorage in a JSON object, like this
      17              :  * {
      18              :  *    content: name → info dict from bridge's /machines Machines property
      19              :  *    overlay: extra data to augment and override on top of content
      20              :  * }
      21              :  *
      22              :  * This uses sessionStorage rather than cockpit.sessionStorage
      23              :  * because we don't ever want to write unprefixed keys.
      24              :  */
      25              : 
      26          123 : const key = cockpit.sessionStorage.prefixedKey("v2-machines.json");
      27          123 : const session_prefix = cockpit.sessionStorage.prefixedKey("v1-session-machine");
      28              : 
      29            0 : function generate_session_key(host) {
      30            0 :     return session_prefix + "/" + host;
      31            0 : }
      32              : 
      33          120 : export function get_init_superuser_for_options(options) {
      34          120 :     let value = null;
      35          120 :     const key = host_superuser_storage_key(options.host);
      36          120 :     if (key)
      37          119 :         value = window.localStorage.getItem(key);
      38              : 
      39              :     /* When connecting, we can optionally try to start a privileged
      40              :      * bridge immediately.  However, it is quite likely that that
      41              :      * needs a password and if we don't have one, it will likely fail.
      42              :      * That would be okay, but sudo is very noisy about failures and
      43              :      * might send nasty emails to your parents.  For that reason we
      44              :      * pass "init-superuser": "none" here when there is no password.
      45              :      *
      46              :      * The downside is that if sudo is configured to not require a
      47              :      * password, we could start it successfully immediately as part
      48              :      * of the connection process, which would be convenient.  However,
      49              :      * if sudo works without password, gaining admin privs is just a
      50              :      * single click, and the convenience loss is not that of a big deal,
      51              :      * hopefully.
      52              :      */
      53              : 
      54           19 :     if (value == "sudo" && !options.password)
      55           19 :         value = "none";
      56              : 
      57          120 :     return value;
      58          120 : }
      59              : 
      60          120 : export function generate_connection_string(user, port, addr) {
      61          120 :     let address = addr;
      62          120 :     if (user)
      63           19 :         address = user + "@" + address;
      64              : 
      65          120 :     if (port)
      66           19 :         address = address + ":" + port;
      67              : 
      68          120 :     return address;
      69          120 : }
      70              : 
      71          131 : export function split_connection_string (conn_to) {
      72          131 :     const parts = { address: "" };
      73          131 :     let user_spot = -1;
      74          131 :     let port_spot = -1;
      75              : 
      76          131 :     if (conn_to) {
      77          131 :         if (conn_to.substring(0, 6) === "ssh://")
      78           30 :             conn_to = conn_to.substring(6);
      79          131 :         user_spot = conn_to.lastIndexOf('@');
      80          131 :         port_spot = conn_to.lastIndexOf(':');
      81          131 :     }
      82              : 
      83           30 :     if (user_spot > 0) {
      84           30 :         parts.user = conn_to.substring(0, user_spot);
      85           30 :         conn_to = conn_to.substring(user_spot + 1);
      86           30 :         port_spot = conn_to.lastIndexOf(':');
      87           30 :     }
      88              : 
      89           30 :     if (port_spot > -1) {
      90           30 :         const port = parseInt(conn_to.substring(port_spot + 1), 10);
      91           30 :         if (!isNaN(port)) {
      92           30 :             parts.port = port;
      93           30 :             conn_to = conn_to.substring(0, port_spot);
      94           30 :         }
      95           30 :     }
      96              : 
      97          131 :     parts.address = conn_to;
      98          131 :     return parts;
      99          131 : }
     100              : 
     101          123 : function import_manifests(val) {
     102          123 :     return validate("manifests", val, import_Manifests, {});
     103          123 : }
     104              : 
     105          123 : function Machines() {
     106          123 :     const self = this;
     107              : 
     108          123 :     cockpit.event_target(self);
     109              : 
     110          123 :     let flat = null;
     111          123 :     self.ready = false;
     112              : 
     113              :     /* parsed machine data */
     114          123 :     const machines = { };
     115              : 
     116              :     /* Data shared between Machines() instances */
     117          123 :     let last = {
     118          123 :         content: null,
     119          123 :         overlay: {
     120          123 :             localhost: {
     121          123 :                 visible: true,
     122          123 :                 manifests: import_manifests(cockpit.manifests)
     123          123 :             }
     124          123 :         }
     125          123 :     };
     126              : 
     127           23 :     function storage(ev) {
     128            1 :         if (ev.key === key && ev.storageArea === window.sessionStorage)
     129            1 :             refresh(JSON.parse(ev.newValue || "null"));
     130           23 :     }
     131              : 
     132          123 :     window.addEventListener("storage", storage);
     133              : 
     134          123 :     window.setTimeout(function() {
     135          123 :         const value = window.sessionStorage.getItem(key);
     136          123 :         if (!self.ready && value)
     137           19 :             refresh(JSON.parse(value));
     138          123 :     });
     139              : 
     140          123 :     let timeout = null;
     141              : 
     142          120 :     function sync(machine, values, overlay) {
     143          120 :         const desired = { ...values, ...overlay };
     144          120 :         for (const prop in desired) {
     145          120 :             if (machine[prop] !== desired[prop])
     146          120 :                 machine[prop] = desired[prop];
     147          120 :         }
     148          120 :         for (const prop in machine) {
     149          120 :             if (machine[prop] !== desired[prop])
     150          120 :                 delete machine[prop];
     151          120 :         }
     152          120 :         return machine;
     153          120 :     }
     154              : 
     155          120 :     function refresh(shared, push) {
     156          120 :         if (!shared)
     157          120 :             return;
     158              : 
     159          120 :         last = shared;
     160          120 :         flat = null;
     161              : 
     162          120 :         if (push && !timeout) {
     163          120 :             timeout = window.setTimeout(function() {
     164          120 :                 timeout = null;
     165          120 :                 window.sessionStorage.setItem(key, JSON.stringify(last));
     166          120 :             }, 10);
     167          120 :         }
     168              : 
     169          120 :         const hosts = { };
     170           19 :         const content = shared.content || { };
     171           19 :         const overlay = shared.overlay || { };
     172          120 :         for (const host in content)
     173           19 :             hosts[host] = true;
     174          120 :         for (const host in overlay)
     175          120 :             hosts[host] = true;
     176              : 
     177          120 :         const events = [];
     178              : 
     179          120 :         for (const host in hosts) {
     180          120 :             const old_machine = machines[host] || { };
     181          120 :             const old_conns = old_machine.connection_string;
     182              : 
     183              :             /* Invert logic for color, always respect what's on disk */
     184           19 :             if (content[host] && content[host].color && overlay[host])
     185           19 :                 delete overlay[host].color;
     186              : 
     187          120 :             const machine = sync(old_machine, content[host], overlay[host]);
     188              : 
     189              :             /* Fill in defaults */
     190          120 :             machine.key = host;
     191          120 :             if (!machine.address)
     192          120 :                 machine.address = host;
     193              : 
     194          120 :             machine.connection_string = generate_connection_string(machine.user,
     195          120 :                                                                    machine.port,
     196          120 :                                                                    machine.address);
     197              : 
     198          120 :             if (!machine.label) {
     199           19 :                 if (host == "localhost" || host == "localhost.localdomain") {
     200          120 :                     const application = cockpit.transport.application();
     201          120 :                     if (application.indexOf('cockpit+=') === 0)
     202           19 :                         machine.label = application.replace('cockpit+=', '');
     203              :                     else
     204          120 :                         machine.label = window.location.hostname;
     205           19 :                 } else {
     206           19 :                     machine.label = host;
     207           19 :                 }
     208          120 :             }
     209          120 :             if (!machine.avatar)
     210          120 :                 machine.avatar = "../shell/images/server-small.png";
     211              : 
     212          120 :             events.push([host in machines ? "updated" : "added",
     213          120 :                 [machine, host, old_conns]]);
     214          120 :             machines[host] = machine;
     215          120 :         }
     216              : 
     217              :         /* Remove any lost hosts */
     218          120 :         for (const host in machines) {
     219           19 :             if (!(host in hosts)) {
     220           19 :                 const machine = machines[host];
     221           19 :                 delete machines[host];
     222           19 :                 delete overlay[host];
     223           19 :                 events.push(["removed", [machine, host]]);
     224           19 :             }
     225          120 :         }
     226              : 
     227              :         /* Fire off all events */
     228          120 :         const len = events.length;
     229          120 :         for (let i = 0; i < len; i++) {
     230          120 :             self.dispatchEvent(events[i][0], ...events[i][1]);
     231          120 :         }
     232          120 :     }
     233              : 
     234            0 :     function update_session_machine(machine, host, values) {
     235              :         /* We don't save the whole machine object */
     236            0 :         const skey = generate_session_key(host);
     237            0 :         const data = { ...machine, ...values };
     238            0 :         window.sessionStorage.setItem(skey, JSON.stringify(data));
     239            0 :         self.overlay(host, values);
     240            0 :         return Promise.resolve([]);
     241            0 :     }
     242              : 
     243            0 :     function update_saved_machine(host, values) {
     244              :         // wrap values in variants for D-Bus call; at least values.port can
     245              :         // be int or string, so stringify everything but the "visible" boolean
     246            0 :         const values_variant = {};
     247            0 :         for (const prop in values) {
     248            0 :             if (values[prop] !== null) {
     249            0 :                 if (prop == "visible")
     250            0 :                     values_variant[prop] = cockpit.variant('b', values[prop]);
     251              :                 else
     252            0 :                     values_variant[prop] = cockpit.variant('s', values[prop].toString());
     253            0 :             }
     254            0 :         }
     255              : 
     256              :         // FIXME: investigate reusing the proxy from Loader (runs in different frame/scope)
     257            0 :         const bridge = cockpit.dbus(null, { bus: "internal", superuser: "require" });
     258            0 :         const mod =
     259            0 :             bridge.call("/machines", "cockpit.Machines", "Update", ["99-webui.json", host, values_variant])
     260            0 :                     .catch(error => {
     261              :                         // avoid make noise when we are not superuser
     262            0 :                         if (error.problem !== "access-denied")
     263            0 :                             console.error("failed to call cockpit.Machines.Update(): ", JSON.stringify(error));
     264            0 :                     })
     265            0 :                     .then(() => self.overlay(host, values));
     266              : 
     267            0 :         return mod;
     268            0 :     }
     269              : 
     270          120 :     self.set_ready = function ready() {
     271          120 :         if (!self.ready) {
     272          120 :             self.ready = true;
     273          120 :             self.dispatchEvent("ready");
     274          120 :         }
     275          120 :     };
     276              : 
     277            0 :     self.add_key = function(host_key) {
     278            0 :         return cockpit.script(ssh_add_key_sh, [host_key.trim(), "known_hosts"], { err: "message" });
     279            0 :     };
     280              : 
     281            0 :     self.add = function add(connection_string, color) {
     282            0 :         let values = split_connection_string(connection_string);
     283            0 :         const host = values.address;
     284              : 
     285            0 :         values = {
     286            0 :             visible: true,
     287            0 :             color: color || self.unused_color(),
     288            0 :             ...values
     289            0 :         };
     290              : 
     291            0 :         const machine = self.lookup(host);
     292            0 :         if (machine)
     293            0 :             machine.on_disk = true;
     294              : 
     295            0 :         return self.change(values.address, values);
     296            0 :     };
     297              : 
     298          112 :     self.unused_color = function unused_color() {
     299          112 :         const len = mod.colors.length;
     300          112 :         for (let i = 0; i < len; i++) {
     301          112 :             if (!color_in_use(mod.colors[i]))
     302          112 :                 return mod.colors[i];
     303          112 :         }
     304           11 :         return "gray";
     305          112 :     };
     306              : 
     307          112 :     function color_in_use(color) {
     308          112 :         const norm = mod.colors.parse(color);
     309          112 :         for (const key in machines) {
     310          112 :             const machine = machines[key];
     311           11 :             if (machine.color && mod.colors.parse(machine.color) == norm)
     312           11 :                 return true;
     313          112 :         }
     314          112 :         return false;
     315          112 :     }
     316              : 
     317          120 :     function merge(item, values) {
     318          120 :         for (const prop in values) {
     319          120 :             if (values[prop] === null)
     320          120 :                 delete item[prop];
     321              :             else
     322          120 :                 item[prop] = values[prop];
     323          120 :         }
     324          120 :     }
     325              : 
     326            0 :     self.change = function change(host, values) {
     327            0 :         const machine = self.lookup(host);
     328              : 
     329            0 :         if (machine && !machine.on_disk)
     330            0 :             return update_session_machine(machine, host, values);
     331              :         else
     332            0 :             return update_saved_machine(host, values);
     333            0 :     };
     334              : 
     335          120 :     self.data = function data(content) {
     336          120 :         const changes = {};
     337              : 
     338           19 :         for (const host in content) {
     339           19 :             changes[host] = { ...last.overlay[host] };
     340           19 :             merge(changes[host], { on_disk: true });
     341           19 :         }
     342              : 
     343              :         /* It's a full reload, so data not
     344              :          * present is no longer from disk
     345              :          */
     346           19 :         for (const host in machines) {
     347           19 :             if (content && !content[host]) {
     348           19 :                 changes[host] = { ...last.overlay[host] };
     349           19 :                 merge(changes[host], { on_disk: null });
     350           19 :             }
     351           19 :         }
     352              : 
     353          120 :         refresh({
     354          120 :             content,
     355          120 :             overlay: { ...last.overlay, ...changes },
     356          120 :         }, true);
     357          120 :     };
     358              : 
     359          120 :     self.overlay = function overlay(host, values) {
     360          120 :         const address = split_connection_string(host).address;
     361          120 :         const changes = { };
     362          120 :         changes[address] = { ...last.overlay[address] };
     363          120 :         merge(changes[address], values);
     364          120 :         refresh({
     365          120 :             content: last.content,
     366          120 :             overlay: { ...last.overlay, ...changes }
     367          120 :         }, true);
     368          120 :     };
     369              : 
     370          123 :     Object.defineProperty(self, "list", {
     371          123 :         enumerable: true,
     372            0 :         get: function get() {
     373            0 :             if (!flat) {
     374            0 :                 flat = [];
     375            0 :                 for (const key in machines) {
     376            0 :                     if (machines[key].visible)
     377            0 :                         flat.push(machines[key]);
     378            0 :                 }
     379            0 :                 flat.sort(function(m1, m2) {
     380            0 :                     return m1.label.localeCompare(m2.label);
     381            0 :                 });
     382            0 :             }
     383            0 :             return flat;
     384            0 :         }
     385          123 :     });
     386              : 
     387          123 :     Object.defineProperty(self, "addresses", {
     388          123 :         enumerable: true,
     389            0 :         get: function get() {
     390            0 :             return Object.keys(machines);
     391            0 :         }
     392          123 :     });
     393              : 
     394          131 :     self.lookup = function lookup(address) {
     395          131 :         const parts = split_connection_string(address);
     396           30 :         return machines[parts.address || "localhost"] || null;
     397          131 :     };
     398              : 
     399            0 :     self.close = function close() {
     400            0 :         window.removeEventListener("storage", storage);
     401            0 :     };
     402          123 : }
     403              : 
     404          123 : function Loader(machines, session_only) {
     405          123 :     const self = this;
     406              : 
     407              :     /* Have we loaded from cockpit session */
     408          123 :     let session_loaded = false;
     409              : 
     410              :     /* echo channels to each machine */
     411          123 :     const channels = { };
     412          123 :     const channels_listeners_message = { };
     413          123 :     const channels_listeners_close = { };
     414              : 
     415              :     /* hostnamed proxies to each machine, if hostnamed available */
     416          123 :     const proxies = { };
     417          123 :     const proxies_listeners_changed = { };
     418              : 
     419              :     /* clients for the bridge D-Bus API */
     420          123 :     const bridge_dbus = { };
     421              : 
     422          119 :     function process_session_key(key, value) {
     423          119 :         const parts = key.split("/");
     424          119 :         if (parts[0] == session_prefix &&
     425           19 :             parts.length === 2) {
     426           19 :             const host = parts[1];
     427           19 :             if (value) {
     428           19 :                 const values = JSON.parse(value);
     429           19 :                 const machine = machines.lookup(host);
     430           19 :                 if (!machine || !machine.on_disk)
     431           19 :                     machines.overlay(host, values);
     432           19 :                 else if (!machine.visible)
     433           19 :                     machines.change(host, { visible: true });
     434           19 :                 self.connect(host);
     435           19 :             }
     436           19 :         }
     437          119 :     }
     438              : 
     439          120 :     function load_from_session_storage() {
     440          120 :         session_loaded = true;
     441          119 :         for (let i = 0; i < window.sessionStorage.length; i++) {
     442          119 :             const k = window.sessionStorage.key(i);
     443          119 :             process_session_key(k, window.sessionStorage.getItem(k));
     444          119 :         }
     445          120 :     }
     446              : 
     447           23 :     function process_session_machines(ev) {
     448           23 :         if (ev.storageArea === window.sessionStorage)
     449            1 :             process_session_key(ev.key || "", ev.newValue);
     450           23 :     }
     451          123 :     window.addEventListener("storage", process_session_machines);
     452              : 
     453          120 :     function state(host, value, problem) {
     454          120 :         const values = { state: value, problem };
     455          120 :         if (value == "connected") {
     456          120 :             values.restarting = false;
     457           19 :         } else if (problem) {
     458           19 :             values.manifests = null;
     459           19 :             values.checksum = null;
     460           19 :             if (problem == "authentication-failed" || problem == "authentication-not-supported")
     461           19 :                 values.restarting = false;
     462           19 :         }
     463          120 :         machines.overlay(host, values);
     464          120 :     }
     465              : 
     466          123 :     machines.addEventListener("added", updated);
     467          123 :     machines.addEventListener("updated", updated);
     468          123 :     machines.addEventListener("removed", removed);
     469              : 
     470          120 :     function updated(ev, machine, host, old_conns) {
     471          120 :         if (!machine) {
     472          120 :             machine = machines.lookup(host);
     473          120 :             if (!machine)
     474          120 :                 return;
     475          120 :         }
     476              : 
     477          120 :         let props = proxies[host];
     478          120 :         if (!props || !props.valid)
     479          120 :             props = { };
     480              : 
     481          120 :         const overlay = { };
     482              : 
     483          120 :         if (!machine.color)
     484          120 :             overlay.color = machines.unused_color();
     485              : 
     486          120 :         const label = props.PrettyHostname || props.StaticHostname || props.Hostname;
     487          120 :         if (label && label !== machine.label)
     488          120 :             overlay.label = label;
     489              : 
     490          120 :         const os = props.OperatingSystemPrettyName;
     491          120 :         if (os && os != machine.os)
     492          120 :             overlay.os = props.OperatingSystemPrettyName;
     493              : 
     494          120 :         if (Object.keys(overlay).length > 0)
     495          120 :             machines.overlay(host, overlay);
     496              : 
     497              :         /* Don't automatically reconnect failed machines, and don't
     498              :          * automatically connect to new machines.  The navigation will
     499              :          * explicitly connect as necessary.
     500              :          */
     501          120 :         if (machine.visible) {
     502           19 :             if (old_conns && machine.connection_string != old_conns) {
     503           19 :                 cockpit.kill(old_conns);
     504           19 :                 self.disconnect(host);
     505           19 :                 self.connect(host);
     506           19 :             }
     507           19 :         } else {
     508           19 :             self.disconnect(host);
     509           19 :         }
     510          120 :     }
     511              : 
     512            0 :     function removed(ev, machine, host) {
     513            0 :         self.disconnect(host);
     514            0 :     }
     515              : 
     516          120 :     self.connect = function connect(host) {
     517          120 :         const machine = machines.lookup(host);
     518          120 :         if (!machine)
     519          120 :             return;
     520              : 
     521          120 :         let channel = channels[host];
     522          120 :         if (channel)
     523          120 :             return;
     524              : 
     525          120 :         const options = {
     526          120 :             host: machine.connection_string,
     527          120 :             payload: "echo",
     528          120 :         };
     529              : 
     530          120 :         options["init-superuser"] = get_init_superuser_for_options(options);
     531              : 
     532           19 :         if (!machine.on_disk && machine.host_key) {
     533           19 :             options['temp-session'] = false; /* Compatibility option */
     534           19 :             options.session = 'shared';
     535           19 :             options['host-key'] = machine.host_key;
     536           19 :         }
     537              : 
     538          120 :         channel = cockpit.channel(options);
     539          120 :         channels[host] = channel;
     540              : 
     541          120 :         const local = host === "localhost";
     542              : 
     543              :         /* Request is null, and message is true when connected */
     544          120 :         let request = null;
     545          120 :         let open = local;
     546              : 
     547          120 :         let url;
     548           19 :         if (!machine.manifests) {
     549           19 :             if (machine.checksum)
     550           19 :                 url = "../../" + machine.checksum + "/manifests.json";
     551              :             else
     552           19 :                 url = "../../@" + encodeURI(machine.connection_string) + "/manifests.json";
     553           19 :         }
     554              : 
     555          120 :         function whirl() {
     556          120 :             if (!request && open)
     557           19 :                 state(host, "connected", null);
     558              :             else
     559           19 :                 state(host, "connecting", null);
     560          120 :         }
     561              : 
     562              :         /* Here we load the machine manifests, and expect them before going to "connected" */
     563            0 :         function request_manifest() {
     564            0 :             request = new XMLHttpRequest();
     565            0 :             request.responseType = "json";
     566            0 :             request.open("GET", url, true);
     567            0 :             request.addEventListener("load", () => {
     568            0 :                 const overlay = { manifests: import_manifests(request.response) };
     569            0 :                 const etag = request.getResponseHeader("ETag");
     570            0 :                 if (etag) /* and remove quotes */
     571            0 :                     overlay.checksum = etag.replace(/^"(.+)"$/, '$1');
     572            0 :                 machines.overlay(host, overlay);
     573              : 
     574            0 :                 request = null;
     575            0 :                 whirl();
     576            0 :             });
     577            0 :             request.addEventListener("error", () => {
     578            0 :                 console.warn("failed to load manifests from " + machine.connection_string);
     579            0 :                 request = null;
     580            0 :                 whirl();
     581            0 :             });
     582            0 :             request.send();
     583            0 :         }
     584              : 
     585              :         /* Try to get change notifications via the internal
     586              :            /packages D-Bus interface of the bridge.  Not all
     587              :            bridges support this API, so we still get the first
     588              :            version of the manifests via HTTP in request_manifest.
     589              :         */
     590              : 
     591          120 :         function watch_manifests() {
     592          120 :             const dbus = cockpit.dbus(null, {
     593          120 :                 bus: "internal",
     594          120 :                 host: machine.connection_string
     595          120 :             });
     596          120 :             bridge_dbus[host] = dbus;
     597          120 :             dbus.subscribe({
     598          120 :                 path: "/packages",
     599          120 :                 interface: "org.freedesktop.DBus.Properties",
     600          120 :                 member: "PropertiesChanged"
     601          120 :             },
     602           11 :                            function (path, iface, member, args) {
     603           11 :                                if (args[0] == "cockpit.Packages") {
     604           11 :                                    if (args[1].Manifests) {
     605           11 :                                        const manifests = JSON.parse(args[1].Manifests.v);
     606           11 :                                        machines.overlay(host, { manifests: import_manifests(manifests) });
     607           11 :                                    }
     608           11 :                                }
     609           11 :                            });
     610              : 
     611              :             /* Tell the bridge to reload the packages, but only if
     612              :                it hasn't just started.  Thus, nothing happens on
     613              :                the first login, but if you reload the shell, we
     614              :                will also reload the packages.
     615              :             */
     616          120 :             dbus.call("/packages", "cockpit.Packages", "ReloadHint", []);
     617          120 :         }
     618              : 
     619          120 :         function request_hostname() {
     620          120 :             if (!machine.static_hostname) {
     621          120 :                 const proxy = cockpit.dbus("org.freedesktop.hostname1",
     622          120 :                                            { host: machine.connection_string }).proxy();
     623          120 :                 proxies[host] = proxy;
     624          120 :                 proxy.wait(function() {
     625            0 :                     proxies_listeners_changed[host] = () => updated(null, null, host);
     626          120 :                     proxy.addEventListener("changed", proxies_listeners_changed[host]);
     627          120 :                     updated(null, null, host);
     628          120 :                 });
     629          120 :             }
     630          120 :         }
     631              : 
     632              :         /* Send a message to the server and get back a message once connected */
     633           19 :         if (!local) {
     634           19 :             channel.send("x");
     635              : 
     636            0 :             channels_listeners_message[host] = () => {
     637            0 :                 open = true;
     638            0 :                 if (url)
     639            0 :                     request_manifest();
     640            0 :                 watch_manifests();
     641            0 :                 request_hostname();
     642            0 :                 whirl();
     643            0 :             };
     644           19 :             channel.addEventListener("message", channels_listeners_message[host]);
     645              : 
     646            0 :             channels_listeners_close[host] = (ev, options) => {
     647            0 :                 const m = machines.lookup(host);
     648            0 :                 open = false;
     649              :                 // reset to clean state when removing machine (orderly disconnect), otherwise mark as failed
     650            0 :                 if (!options.problem && m && !m.visible)
     651            0 :                     state(host, null, null);
     652              :                 else
     653            0 :                     state(host, "failed", options.problem || "disconnected");
     654            0 :                 if (m && m.restarting) {
     655            0 :                     window.setTimeout(function() {
     656            0 :                         self.connect(host);
     657            0 :                     }, 10000);
     658            0 :                 }
     659            0 :                 self.disconnect(host);
     660            0 :             };
     661           19 :             channel.addEventListener("close", channels_listeners_close[host]);
     662           19 :         } else {
     663          120 :             if (url)
     664           19 :                 request_manifest();
     665          120 :             watch_manifests();
     666          120 :             request_hostname();
     667          120 :         }
     668              : 
     669              :         /* In case already ready, for example when local */
     670          120 :         whirl();
     671          120 :     };
     672              : 
     673            0 :     self.disconnect = function disconnect(host) {
     674            0 :         if (host === "localhost")
     675            0 :             return;
     676              : 
     677            0 :         const channel = channels[host];
     678            0 :         delete channels[host];
     679            0 :         if (channel) {
     680            0 :             channel.close();
     681            0 :             channel.removeEventListener("message", channels_listeners_message[host]);
     682            0 :             channel.removeEventListener("close", channels_listeners_close[host]);
     683            0 :         }
     684              : 
     685            0 :         const proxy = proxies[host];
     686            0 :         delete proxies[host];
     687            0 :         if (proxy) {
     688            0 :             proxy.client.close();
     689            0 :             proxy.removeEventListener("changed", proxies_listeners_changed[host]);
     690            0 :         }
     691              : 
     692            0 :         const dbus = bridge_dbus[host];
     693            0 :         delete bridge_dbus[host];
     694            0 :         if (dbus) {
     695            0 :             dbus.close();
     696            0 :         }
     697            0 :     };
     698              : 
     699            0 :     self.expect_restart = function expect_restart(host) {
     700            0 :         const parts = split_connection_string(host);
     701            0 :         machines.overlay(parts.address, {
     702            0 :             restarting: true,
     703            0 :             problem: null
     704            0 :         });
     705            0 :     };
     706              : 
     707            0 :     self.close = function close() {
     708            0 :         machines.removeEventListener("added", updated);
     709            0 :         machines.removeEventListener("changed", updated);
     710            0 :         machines.removeEventListener("removed", removed);
     711            0 :         machines = null;
     712              : 
     713            0 :         window.removeEventListener("storage", process_session_machines);
     714            0 :         const hosts = Object.keys(channels);
     715            0 :         hosts.forEach(self.disconnect);
     716            0 :     };
     717              : 
     718          123 :     if (!session_only) {
     719          123 :         const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Machines", "/machines");
     720          120 :         proxy.addEventListener("changed", data => {
     721              :             // unwrap variants from D-Bus call
     722          120 :             const wrapped = proxy.Machines;
     723          120 :             cockpit.assert(typeof wrapped === "object" && wrapped !== null, "unexpected type of Machines property");
     724          120 :             const data_unwrap = {};
     725           19 :             for (const host in wrapped) {
     726           19 :                 const host_props = {};
     727           19 :                 for (const prop in wrapped[host])
     728           19 :                     host_props[prop] = wrapped[host][prop].v;
     729           19 :                 data_unwrap[host] = host_props;
     730           19 :             }
     731              : 
     732          120 :             machines.data(data_unwrap);
     733          120 :             if (!session_loaded)
     734          120 :                 load_from_session_storage();
     735          120 :             machines.set_ready();
     736          120 :         });
     737           19 :     } else {
     738           19 :         load_from_session_storage();
     739           19 :         machines.data({});
     740           19 :         machines.set_ready();
     741           19 :     }
     742          123 : }
     743              : 
     744          123 : mod.instance = function instance(loader) {
     745          123 :     return new Machines();
     746          123 : };
     747              : 
     748          123 : mod.loader = function loader(machines, session_only) {
     749          123 :     return new Loader(machines, session_only);
     750          123 : };
     751              : 
     752          123 : mod.colors = [
     753          123 :     "#0099d3",
     754          123 :     "#67d300",
     755          123 :     "#d39e00",
     756          123 :     "#d3007c",
     757          123 :     "#00d39f",
     758          123 :     "#00d1d3",
     759          123 :     "#00618a",
     760          123 :     "#4c8a00",
     761          123 :     "#8a6600",
     762          123 :     "#9b005b",
     763          123 :     "#008a55",
     764          123 :     "#008a8a",
     765          123 :     "#00b9ff",
     766          123 :     "#7dff00",
     767          123 :     "#ffbe00",
     768          123 :     "#ff0096",
     769          123 :     "#00ffc0",
     770          123 :     "#00fdff",
     771          123 :     "#023448",
     772          123 :     "#264802",
     773          123 :     "#483602",
     774          123 :     "#590034",
     775          123 :     "#024830",
     776          123 :     "#024848"
     777          123 : ];
     778              : 
     779          112 : mod.colors.parse = function parse_color(input) {
     780          112 :     const div = document.createElement('div');
     781          112 :     div.style.color = input;
     782          112 :     const style = window.getComputedStyle(div, null);
     783          112 :     return style.getPropertyValue("color") || div.style.color;
     784          112 : };
     785              : 
     786          123 : export const machines = mod;
        

Generated by: LCOV version 2.0-1