LCOV - code coverage report
Current view: top level - pkg/shell - state.tsx Coverage Total Hit
Test: cockpit Lines: 86.3 % 314 271
Test Date: 2026-06-16 14:09:37

            Line data    Source code
       1          123 : /*
       2              :  * Copyright (C) 2024 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          123 : import cockpit from "cockpit";
       7              : 
       8              : import { EventEmitter } from "cockpit/event";
       9              : import { Status } from "notifications";
      10              : 
      11              : import { Router } from "./router.jsx";
      12              : import {
      13              :     machines as machines_factory,
      14              :     Machine, Machines, Loader
      15              : } from "./machines/machines.js";
      16              : import {
      17              :     decode_location, decode_window_location, push_window_location, replace_window_location,
      18              :     compile_manifests, compute_frame_url,
      19              :     Location, ManifestItem, CompiledComponents,
      20              : } from "./util.jsx";
      21              : import { Manifest, ShellManifest, import_ShellManifest } from "./manifests";
      22              : import { validate } from "import-json";
      23              : 
      24              : export interface ShellConfig {
      25              :     language: string;
      26              :     language_direction: string;
      27              :     host_switcher_enabled: boolean;
      28              :     manifest: ShellManifest;
      29              : }
      30              : 
      31              : export interface ShellFrame {
      32              :     name: string;
      33              :     host: string;
      34              :     path: string;
      35              :     title: string;
      36              :     url: string | null;
      37              :     hash: string;
      38              :     ready: boolean;
      39              :     loaded: boolean;
      40              : }
      41              : 
      42              : export interface ShellStateEvents {
      43              :     update: () => void;
      44              :     connect: () => void;
      45              : }
      46              : 
      47          123 : export class ShellState extends EventEmitter<ShellStateEvents> {
      48          123 :     constructor() {
      49          123 :         super();
      50          123 :         this.config = this.#init_config();
      51              : 
      52          123 :         this.machines = this.#init_machines();
      53          123 :         this.loader = this.#init_loader();
      54          123 :         this.router = this.#init_router();
      55              : 
      56          123 :         this.#init_oops();
      57          123 :         this.#init_page_status();
      58              : 
      59          123 :         this.#on_ready();
      60          123 :     }
      61              : 
      62              :     /* READINESS STATE
      63              :      */
      64              : 
      65          123 :     ready: boolean = false;
      66          123 :     has_oops: boolean = false;
      67              : 
      68          123 :     #on_ready() {
      69          120 :         if (this.machines.ready && this.#config_ready) {
      70          120 :             this.ready = true;
      71            0 :             window.addEventListener("popstate", () => {
      72            0 :                 this.update();
      73            0 :                 this.ensure_frame_loaded();
      74            0 :                 this.ensure_connection();
      75            0 :             });
      76              : 
      77          120 :             this.update();
      78          120 :             this.ensure_frame_loaded();
      79          120 :             this.ensure_connection();
      80          120 :         }
      81          123 :     }
      82              : 
      83              :     /* CONFIG
      84              :      */
      85              : 
      86              :     config: ShellConfig;
      87              : 
      88          123 :     #config_ready: boolean = false;
      89              : 
      90          123 :     #init_config() {
      91          123 :         let language = document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1");
      92          123 :         if (!language)
      93          123 :             language = navigator.language.toLowerCase(); // Default to Accept-Language header
      94              : 
      95          123 :         const config = {
      96          123 :             language,
      97          123 :             language_direction: cockpit.language_direction,
      98          123 :             host_switcher_enabled: false,
      99          123 :             manifest: validate("manifests.shell", cockpit.manifests.shell, import_ShellManifest,
     100          123 :                                { docs: undefined, locales: undefined }),
     101          123 :         };
     102              : 
     103              :         /* Host switcher enabled? */
     104          123 :         const meta_multihost = document.head.querySelector("meta[name='allow-multihost']");
     105          123 :         if (meta_multihost instanceof HTMLMetaElement && meta_multihost.content == "yes")
     106           19 :             config.host_switcher_enabled = true;
     107              : 
     108              :         /* Should show warning before connecting? */
     109          123 :         this.#config_ready = false;
     110          123 :         cockpit.dbus(null, { bus: "internal" }).call("/config", "cockpit.Config", "GetString",
     111          123 :                                                      ["Session", "WarnBeforeConnecting"], {})
     112            0 :                 .then(([result]) => {
     113            0 :                     if (result == "false" || result == "no") {
     114            0 :                         window.sessionStorage.setItem("connection-warning-shown", "yes");
     115            0 :                     }
     116            0 :                 })
     117          123 :                 .catch(e => {
     118          123 :                     if (e.name != "cockpit.Config.KeyError")
     119           22 :                         console.warn("Error reading WarnBeforeConnecting configuration:", e.message);
     120          123 :                 })
     121          123 :                 .finally(() => {
     122          123 :                     this.#config_ready = true;
     123          123 :                     this.#on_ready();
     124          123 :                 });
     125              : 
     126          123 :         return config;
     127          123 :     }
     128              : 
     129              :     /* MACHINES AND LOADER
     130              :      *
     131              :      * These are part of the machinery in the basement that maintains
     132              :      * the database of all hosts (including "localhost"), and monitors
     133              :      * their manifests.
     134              :      */
     135              : 
     136              :     machines: Machines;
     137              :     loader: Loader;
     138              : 
     139          123 :     #init_machines() {
     140          123 :         const machines = machines_factory.instance();
     141              : 
     142          120 :         machines.addEventListener("ready", () => this.#on_ready());
     143              : 
     144            0 :         machines.addEventListener("removed", (_, machine) => {
     145            0 :             this.#remove_machine_frames(machine);
     146            0 :         });
     147          120 :         machines.addEventListener("added", (_, machine) => {
     148          120 :             this.#preload_machine_frames(machine);
     149          120 :         });
     150          120 :         machines.addEventListener("updated", (_, machine) => {
     151          120 :             if (!machine.visible || machine.problem)
     152           19 :                 this.#remove_machine_frames(machine);
     153              :             else
     154          120 :                 this.#preload_machine_frames(machine);
     155          120 :         });
     156              : 
     157          123 :         return machines;
     158          123 :     }
     159              : 
     160          123 :     #init_loader() {
     161          123 :         return machines_factory.loader(this.machines);
     162          123 :     }
     163              : 
     164              :     /* OOPS HANDLING
     165              :      */
     166              : 
     167          123 :     #init_oops() {
     168          123 :         const old_onerror = window.onerror;
     169            0 :         window.onerror = (msg, url, line) => {
     170              :             // Errors with url == "" are not logged apparently, so let's
     171              :             // not show the "Oops" for them either.
     172            0 :             if (url != "") {
     173            0 :                 this.has_oops = true;
     174            0 :                 this.update();
     175            0 :             }
     176            0 :             if (old_onerror)
     177            0 :                 return old_onerror(msg, url, line);
     178            0 :             return false;
     179            0 :         };
     180          123 :     }
     181              : 
     182              :     /* FRAMES
     183              :      *
     184              :      * Frames are created on-demand when navigating to them for the
     185              :      * first time, by calling ensure_frame().
     186              :      *
     187              :      * Once a frame object is created it doesn't change anymore except
     188              :      * for its "ready", "loaded", and "hash" properties.
     189              :      *
     190              :      * The "ready" property starts out false and goes to true once the
     191              :      * corresponding iframe has loaded its URL. The "loaded" property
     192              :      * starts out false and goes true once the code loaded into the
     193              :      * frame has sent its "init" message.
     194              :      *
     195              :      * Removing things (frames) is complicated, as usual.  We need to
     196              :      * be able to represent the state "The current frame has been
     197              :      * removed" without any call to update() re-creating it
     198              :      * spontaneously. Thus, a frame has a special "dead" state where
     199              :      * its "url" property is null. Actually clicking on navigation
     200              :      * elements will call the "ensure_frame_loaded" hook, which will
     201              :      * bring the current frame back to life if necessary. This happens
     202              :      * in the "jump" method.
     203              :      */
     204              : 
     205          123 :     frames: { [name: string]: ShellFrame } = { };
     206              : 
     207          131 :     #ensure_frame(machine: Machine, path: string, hash: string | null, title: string): ShellFrame | null {
     208              :         /* Never create new frames for machines that are not
     209              :            connected yet. That would open a channel to them (for
     210              :            loading the URL), which woould trigger the bridge to
     211              :            attempt a log in. We want all logins to happen in a
     212              :            single place (in hosts.jsx) so that we can get the
     213              :            options right, and show a warning dialog.
     214              :         */
     215           30 :         if (machine.address != "localhost" && machine.state !== "connected")
     216           30 :             return null;
     217              : 
     218          131 :         const name = "cockpit1:" + machine.connection_string + "/" + path;
     219          131 :         let frame = this.frames[name];
     220              : 
     221          131 :         if (!frame) {
     222          131 :             frame = this.frames[name] = {
     223          131 :                 name,
     224          131 :                 host: machine.address,
     225          131 :                 path,
     226          131 :                 url: compute_frame_url(machine, path),
     227          131 :                 hash: hash || "/",
     228          131 :                 title,
     229          131 :                 ready: false,
     230          131 :                 loaded: false,
     231          131 :             };
     232          131 :         } else {
     233              :             // XXX - shouldn't we leave the hash alone when it is null here?
     234          131 :             frame.hash = hash || "/";
     235          131 :         }
     236          131 :         return frame;
     237          131 :     }
     238              : 
     239          120 :     ensure_frame_loaded (): void {
     240           19 :         if (this.current_frame && this.current_frame.url == null) {
     241              :             // Let update() recreate the frame.
     242           19 :             delete this.frames[this.current_frame.name];
     243           19 :             this.current_frame = null;
     244           19 :             this.update();
     245           19 :         }
     246          120 :     }
     247              : 
     248            0 :     #kill_frame(name: string): void {
     249              :         // Only mark frame as dead, it gets removed for real during
     250              :         // the call to "update".
     251            0 :         this.frames[name].url = null;
     252            0 :     }
     253              : 
     254            0 :     remove_frame (name: string): void {
     255            0 :         this.#kill_frame(name);
     256            0 :         this.update();
     257            0 :     }
     258              : 
     259            0 :     #remove_machine_frames (machine: Machine): void {
     260            0 :         const names = Object.keys(this.frames);
     261            0 :         for (const n of names) {
     262            0 :             if (this.frames[n].host == machine.address)
     263            0 :                 this.#kill_frame(n);
     264            0 :         }
     265            0 :         this.update();
     266            0 :     }
     267              : 
     268          120 :     #preload_machine_frames (machine: Machine) {
     269          120 :         const manifests = machine.manifests;
     270          120 :         const compiled = compile_manifests(manifests);
     271          120 :         for (const c in manifests) {
     272          120 :             const preload = manifests[c].preload as unknown as string[];
     273          120 :             if (preload && preload.length) {
     274          120 :                 for (const p of preload) {
     275           21 :                     const path = (p == "index") ? c : c + "/" + p;
     276          120 :                     const item = compiled.find_path_item(path);
     277          120 :                     this.#ensure_frame(machine, path, null, item.label);
     278          120 :                 }
     279          120 :             }
     280          120 :         }
     281          120 :         this.update();
     282          120 :     }
     283              : 
     284              :     /* PAGE STATUS
     285              :      *
     286              :      * Page status notifications arrive from the Router (see
     287              :      * below). We also store them in the session storage so that
     288              :      * individual pages have access to all collected statuses.
     289              :      */
     290              : 
     291          123 :     page_status: { [host: string]: { [page: string]: Status } } = { };
     292              : 
     293          123 :     #init_page_status() {
     294          123 :         sessionStorage.removeItem("cockpit:page_status");
     295          123 :     }
     296              : 
     297            8 :     #notify_page_status(host: string, page: string, status: Status) {
     298            8 :         if (!this.page_status[host])
     299            8 :             this.page_status[host] = { };
     300            8 :         this.page_status[host][page] = status;
     301            8 :         sessionStorage.setItem("cockpit:page_status", JSON.stringify(this.page_status));
     302            8 :         this.update();
     303            8 :     }
     304              : 
     305              :     /* ROUTER
     306              :      *
     307              :      * The router is the machinery in our basement that forwards
     308              :      * Cockpit protocol messages between the WebSocket and the
     309              :      * frames. Some messages are also meant for the Shell itself, and
     310              :      * we pass a big object with callback function to the router to
     311              :      * process these and other noteworthy events.
     312              :      */
     313              : 
     314              :     router: Router;
     315              : 
     316          123 :     #init_router() {
     317          123 :         const callbacks = {
     318              :             /* The router has just processed the "init" message of the
     319              :              * code loaded into the frame named FRAME_NAME.
     320              :              *
     321              :              * We set the "loaded" property to help the tests, and also
     322              :              * tell the frame whether it is visible or not.
     323              :              */
     324          120 :             frame_is_initialized: (frame_name: string) => {
     325          120 :                 const frame = this.frames[frame_name];
     326          120 :                 if (frame) {
     327          120 :                     frame.loaded = true;
     328          120 :                     this.update();
     329          120 :                 }
     330          120 :                 this.#send_frame_hidden_hint(frame_name);
     331          120 :             },
     332              : 
     333              :             /* The frame named FRAME_NAME wants the shell to jump to
     334              :              * LOCATION.
     335              :              *
     336              :              * Only requests from the current frame are honored.  But the
     337              :              * tests also use this extensively for navigation, and might
     338              :              * send messages from the top-most window, which we know is
     339              :              * named "cockpit1".
     340              :              */
     341           24 :             perform_frame_jump_command: (frame_name: string, location: string) => {
     342           22 :                 if (frame_name == "cockpit1" || (this.current_frame && this.current_frame.name == frame_name)) {
     343           24 :                     this.jump(location);
     344           24 :                     this.ensure_connection();
     345           24 :                 }
     346           24 :             },
     347              : 
     348              :             /* The frame named FRAME_NAMED has just changed the hash part
     349              :              * of its URL. That's how frames navigate within themselves.
     350              :              *
     351              :              * When the current frame does that, we need to reflect the
     352              :              * hash change in the shell URL as well.
     353              :              */
     354          120 :             perform_frame_hash_track: (frame_name: string, hash: string) => {
     355              :                 /* Note that we ignore tracking for old shell code */
     356          120 :                 if (this.current_frame && this.current_frame.name === frame_name &&
     357          119 :                     frame_name && frame_name.indexOf("/shell/shell") === -1) {
     358              :                     /* The browser has already pushed an appropriate entry to
     359              :                        the history, so let's just replace it with one that
     360              :                        includes the right hash.
     361              :                      */
     362          119 :                     const location = Object.assign({}, decode_window_location(), { hash });
     363          119 :                     replace_window_location(location);
     364          119 :                     this.#remember_location(location.host, location.path, location.hash);
     365          119 :                     this.update();
     366          119 :                 }
     367          120 :             },
     368              : 
     369              :             /* A notification has been received from a frame. We only
     370              :              * handle page status notifications, such as the ones that
     371              :              * tell you when software updates are available.  PAGE is the
     372              :              * "well-known name" of a page, such as "system",
     373              :              * "network/firewall", or "updates".
     374              :              */
     375            8 :             handle_notifications: (host: string, page: string, data: { page_status?: Status }) => {
     376            8 :                 if (data.page_status !== undefined)
     377            8 :                     this.#notify_page_status(host, page, data.page_status);
     378            8 :             },
     379              : 
     380              :             /* One of the frames has experienced a unhandled JavaScript exception.
     381              :              */
     382            1 :             show_oops: () => {
     383            1 :                 this.has_oops = true;
     384            1 :                 this.update();
     385            1 :             },
     386              : 
     387              :             /* The host with address HOST has just initiated a restart. We
     388              :              * tell the loader.
     389              :              */
     390            0 :             expect_restart: (host: string) => {
     391            0 :                 this.loader.expect_restart(host);
     392            0 :             },
     393          123 :         };
     394              : 
     395          123 :         return new Router(callbacks);
     396          123 :     }
     397              : 
     398          120 :     #send_frame_hidden_hint (frame_name: string) {
     399          120 :         const hidden = !this.current_frame || this.current_frame.name != frame_name;
     400          120 :         this.router.hint(frame_name, { hidden });
     401          120 :     }
     402              : 
     403              :     /* NAVIGATION
     404              :      *
     405              :      * The main navigation function, jump(), will change
     406              :      * window.location as requested and then trigger a general
     407              :      * ShellState update. The update processing will look at
     408              :      * window.location and update the various "current_*" properties
     409              :      * of the shell state accordingly.  (The update processing might
     410              :      * also change window.location again itself, in order to
     411              :      * canonicalize it.)
     412              :      *
     413              :      * The new location given to jump() can be partial; the missing
     414              :      * pieces are filled in from the browsing history in a (almost)
     415              :      * natural way. If the HOST part is missing, it will be taken from
     416              :      * the current location. If the PATH part is missing, the last
     417              :      * path visited on the given host is used. And if the HASH is
     418              :      * missing, the last one from the given HOST/PATH combination is
     419              :      * used. But only, and this is a historical quirk, when the new
     420              :      * host/path differs from the current host/path. Don't rely on
     421              :      * that, always use "/" as the hash when jumping to the top
     422              :      * sub-page.
     423              :      *
     424              :      * Calling jump() will also make sure that the (newly) current
     425              :      * frame will now be loaded again in the case that it was
     426              :      * explicitly removed earlier. (This also happens when
     427              :      * window.location isn't actually changed by jump().)
     428              :      *
     429              :      * But jump() will never open a new connection to a HOST that is
     430              :      * not yet connected. If you want that, call ensure_connection()
     431              :      * right after jump().  However, it is better to first connect to
     432              :      * the host using the connect_host function from hosts_dialog.jsx
     433              :      * and only call jump() when that has succeeded.
     434              :      *
     435              :      * Calling ensure_connection() will start a user interaction to
     436              :      * open a connection to the host of the current navigation
     437              :      * location, but will not wait for this to be complete.
     438              :      */
     439              : 
     440          123 :     #last_path_for_host: Record<string, string> = { };
     441          123 :     #last_hash_for_host_path: Record<string, Record<string, string>> = { };
     442              : 
     443            0 :     most_recent_path_for_host(host: string) {
     444            0 :         return this.#last_path_for_host[host] || "";
     445            0 :     }
     446              : 
     447           12 :     #most_recent_hash_for_path(host: string, path: string) {
     448           12 :         if (this.#last_hash_for_host_path[host])
     449           12 :             return this.#last_hash_for_host_path[host][path] || null;
     450            1 :         return null;
     451           12 :     }
     452              : 
     453          131 :     #remember_location(host: string, path: string, hash: string) {
     454          131 :         this.#last_path_for_host[host] = path;
     455          131 :         if (!this.#last_hash_for_host_path[host])
     456          131 :             this.#last_hash_for_host_path[host] = { };
     457          131 :         this.#last_hash_for_host_path[host][path] = hash;
     458          131 :     }
     459              : 
     460           26 :     jump (location: Partial<Location> | string): boolean {
     461           26 :         if (typeof location === "string")
     462           24 :             location = decode_location(location);
     463              : 
     464           26 :         const current = decode_window_location();
     465              : 
     466              :         /* Fill in the missing pieces, in order.
     467              :          */
     468              : 
     469           26 :         if (!location.host)
     470            1 :             location.host = current.host || "localhost";
     471              : 
     472           26 :         if (!location.path)
     473            1 :             location.path = this.most_recent_path_for_host(location.host);
     474              : 
     475           16 :         if (!location.hash) {
     476           16 :             if (location.host != current.host || location.path != current.path)
     477            2 :                 location.hash = this.#most_recent_hash_for_path(location.host, location.path) || "/";
     478              :             else
     479            6 :                 console.warn('Shell jump with hash and no frame change. Please use "/" as the hash to jump to the top sub-page.');
     480           16 :         }
     481              : 
     482           26 :         if (location.host !== current.host ||
     483           26 :             location.path !== current.path ||
     484           12 :             location.hash !== current.hash) {
     485           26 :             push_window_location(location);
     486           26 :             this.update();
     487           26 :             this.ensure_frame_loaded();
     488           26 :             return true;
     489           26 :         }
     490              : 
     491            3 :         this.ensure_frame_loaded();
     492            3 :         return false;
     493           26 :     }
     494              : 
     495          120 :     ensure_connection() {
     496          120 :         if (this.current_machine) {
     497              :             // Handle localhost right here, we never need user
     498              :             // interactions for it, and it is kind of important to not
     499              :             // mess up connecting to localhost. So we avoid relying on
     500              :             // the bigger machinery for it.
     501              :             //
     502          120 :             if (this.current_machine.connection_string == "localhost") {
     503          120 :                 this.loader.connect("localhost");
     504          120 :                 return;
     505          120 :             }
     506              : 
     507           19 :             this.emit("connect");
     508           19 :         }
     509          120 :     }
     510              : 
     511              :     /* STATE
     512              :      *
     513              :      * Whenever the shell state changes, the "updated" event is
     514              :      * dispatched.
     515              :      *
     516              :      * The main part of the shell state is the information related to
     517              :      * the current navigation location:
     518              :      *
     519              :      * - current_location
     520              :      *
     521              :      * A object with "host", "path", and "hash" fields that reflect
     522              :      * the current location. "hash" does not have the "#" character.
     523              :      *
     524              :      * - current_machine
     525              :      *
     526              :      * The machine object (see machines/machines.js) for the "host"
     527              :      * part of "current_location". This is never null when
     528              :      * "current_location" isn't null. But the machine might not be
     529              :      * connected, and might not have manifests, etc.
     530              :      *
     531              :      * - current_manifest_item
     532              :      *
     533              :      * The manifest item corresponding to the "path" part of
     534              :      * "current_location". This is a piece of the current machines
     535              :      * manifests, from the "menu", "tools", or "dashboard" arrays.
     536              :      *
     537              :      * The item describes the navigation item in the sidebar that gets
     538              :      * highlighted for "path". The correspondence between the two is
     539              :      * not always straightforward. For example, both "network" and
     540              :      * "network/firewall" will have the same item, the one for
     541              :      * "Networking". But "system/logs" has its own item, "Logs",
     542              :      * even though it comes from the same package as the "system" path.
     543              :      *
     544              :      * And then, the "metrics" path has the "Overview" item associated
     545              :      * with it, although the two come from different packages.
     546              :      *
     547              :      * - current_manifest
     548              :      *
     549              :      * The manifest corresponding to the "path" part of
     550              :      * "current_location". The "current_manifest_item" is not
     551              :      * necessarily part of this manifest, but this manifest is always
     552              :      * from the same package as the files loaded for the current
     553              :      * location.
     554              :      *
     555              :      * For example, for the "metrics" path the "current_manifest" will
     556              :      * be for the "metrics" package, while "current_manifest_item" is
     557              :      * for the "Overview" menu entry from the "system" package.
     558              :      */
     559              : 
     560          123 :     current_location: Location | null = null;
     561          123 :     current_machine: Machine | null = null;
     562          123 :     current_manifest_item: ManifestItem | null = null;
     563          123 :     current_machine_manifest_items: CompiledComponents | null = null;
     564          123 :     current_manifest: Manifest | null = null;
     565              : 
     566          123 :     current_frame: ShellFrame | null = null;
     567              : 
     568          131 :     update() {
     569          131 :         if (!this.ready) {
     570          131 :             this.emit("update");
     571          131 :             return;
     572          131 :         }
     573              : 
     574          131 :         const location = decode_window_location();
     575              : 
     576              :         // Force a redirect to localhost when the host switcher is
     577              :         // disabled. That way, people won't accidentally connect to
     578              :         // remote machines via URL bookmarks or similar that point to
     579              :         // them.
     580          131 :         if (!this.config.host_switcher_enabled) {
     581          131 :             location.host = "localhost";
     582          131 :             replace_window_location(location);
     583          131 :         }
     584              : 
     585          131 :         let machine = this.machines.lookup(location.host);
     586              : 
     587              :         /* No such machine */
     588           30 :         if (!machine || !machine.visible) {
     589           30 :             machine = {
     590           30 :                 key: location.host,
     591           30 :                 connection_string: location.host,
     592           30 :                 address: location.host,
     593           30 :                 label: location.host,
     594           30 :                 state: "failed",
     595           30 :                 problem: "not-found",
     596           30 :             };
     597           30 :         }
     598              : 
     599          131 :         const compiled = compile_manifests(machine.manifests);
     600           35 :         if (machine.manifests && !location.path) {
     601              :             // Find the default path based on the manifest.
     602           35 :             const menu_items = compiled.ordered("menu");
     603           35 :             if (menu_items.length > 0 && menu_items[0])
     604           30 :                 location.path = menu_items[0].path;
     605              :             else
     606           30 :                 location.path = "system";
     607           35 :             replace_window_location(location);
     608           35 :         }
     609              : 
     610              :         // Remember the most recent history for each host, and each
     611              :         // host/path combination.  This is used by JUMP to complete
     612              :         // partial locations.
     613              :         //
     614          131 :         this.#remember_location(location.host, location.path, location.hash);
     615              : 
     616          131 :         const item = compiled.find_path_item(location.path);
     617              : 
     618          131 :         this.current_location = location;
     619          131 :         this.current_machine = machine;
     620          131 :         this.current_machine_manifest_items = compiled;
     621          131 :         this.current_manifest_item = item;
     622          131 :         this.current_manifest = compiled.find_path_manifest(location.path);
     623              : 
     624          131 :         let frame = null;
     625          131 :         if (location.path && (machine.state == "connected" || machine.state == "connecting"))
     626          131 :             frame = this.#ensure_frame(machine, location.path, location.hash, item.label);
     627              : 
     628          131 :         if (frame != this.current_frame) {
     629          131 :             const prev_frame = this.current_frame;
     630          131 :             this.current_frame = frame;
     631              : 
     632          131 :             if (prev_frame)
     633           44 :                 this.#send_frame_hidden_hint(prev_frame.name);
     634          131 :             if (frame)
     635          131 :                 this.#send_frame_hidden_hint(frame.name);
     636          131 :         }
     637              : 
     638              :         // Remove all dead frames that are not the current one.
     639          131 :         for (const n of Object.keys(this.frames)) {
     640           30 :             if (this.frames[n].url == null && this.frames[n] != this.current_frame)
     641           30 :                 delete this.frames[n];
     642          131 :         }
     643              : 
     644          131 :         this.emit("update");
     645          131 :     }
     646          123 : }
        

Generated by: LCOV version 2.0-1