LCOV - code coverage report
Current view: top level - pkg/shell/machines - machines.js Coverage Total Hit
Test: cockpit Lines: 94.7 % 589 558
Test Date: 2026-07-13 10:00:01

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

Generated by: LCOV version 2.0-1