LCOV - code coverage report
Current view: top level - pkg/shell - state.tsx Coverage Total Hit
Test: cockpit Lines: 94.9 % 314 298
Test Date: 2026-07-17 14:32:59

            Line data    Source code
       1          341 : /*
       2              :  * Copyright (C) 2024 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          341 : 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          341 : export class ShellState extends EventEmitter<ShellStateEvents> {
      48          341 :     constructor() {
      49          341 :         super();
      50          341 :         this.config = this.#init_config();
      51              : 
      52          341 :         this.machines = this.#init_machines();
      53          341 :         this.loader = this.#init_loader();
      54          341 :         this.router = this.#init_router();
      55              : 
      56          341 :         this.#init_oops();
      57          341 :         this.#init_page_status();
      58              : 
      59          341 :         this.#on_ready();
      60          341 :     }
      61              : 
      62              :     /* READINESS STATE
      63              :      */
      64              : 
      65          341 :     ready: boolean = false;
      66          341 :     has_oops: boolean = false;
      67              : 
      68          341 :     #on_ready() {
      69          338 :         if (this.machines.ready && this.#config_ready) {
      70          338 :             this.ready = true;
      71            1 :             window.addEventListener("popstate", () => {
      72            1 :                 this.update();
      73            1 :                 this.ensure_frame_loaded();
      74            1 :                 this.ensure_connection();
      75            1 :             });
      76              : 
      77          338 :             this.update();
      78          338 :             this.ensure_frame_loaded();
      79          338 :             this.ensure_connection();
      80          338 :         }
      81          341 :     }
      82              : 
      83              :     /* CONFIG
      84              :      */
      85              : 
      86              :     config: ShellConfig;
      87              : 
      88          341 :     #config_ready: boolean = false;
      89              : 
      90          341 :     #init_config() {
      91          341 :         let language = document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1");
      92          341 :         if (!language)
      93          341 :             language = navigator.language.toLowerCase(); // Default to Accept-Language header
      94              : 
      95          341 :         const config = {
      96          341 :             language,
      97          341 :             language_direction: cockpit.language_direction,
      98          341 :             host_switcher_enabled: false,
      99          341 :             manifest: validate("manifests.shell", cockpit.manifests.shell, import_ShellManifest,
     100          341 :                                { docs: undefined, locales: undefined }),
     101          341 :         };
     102              : 
     103              :         /* Host switcher enabled? */
     104          341 :         const meta_multihost = document.head.querySelector("meta[name='allow-multihost']");
     105          341 :         if (meta_multihost instanceof HTMLMetaElement && meta_multihost.content == "yes")
     106           76 :             config.host_switcher_enabled = true;
     107              : 
     108              :         /* Should show warning before connecting? */
     109          341 :         this.#config_ready = false;
     110          341 :         cockpit.dbus(null, { bus: "internal" }).call("/config", "cockpit.Config", "GetString",
     111          341 :                                                      ["Session", "WarnBeforeConnecting"], {})
     112            1 :                 .then(([result]) => {
     113            0 :                     if (result == "false" || result == "no") {
     114            1 :                         window.sessionStorage.setItem("connection-warning-shown", "yes");
     115            1 :                     }
     116            1 :                 })
     117          340 :                 .catch(e => {
     118          340 :                     if (e.name != "cockpit.Config.KeyError")
     119           65 :                         console.warn("Error reading WarnBeforeConnecting configuration:", e.message);
     120          340 :                 })
     121          341 :                 .finally(() => {
     122          341 :                     this.#config_ready = true;
     123          341 :                     this.#on_ready();
     124          341 :                 });
     125              : 
     126          341 :         return config;
     127          341 :     }
     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          341 :     #init_machines() {
     140          341 :         const machines = machines_factory.instance();
     141              : 
     142          338 :         machines.addEventListener("ready", () => this.#on_ready());
     143              : 
     144            0 :         machines.addEventListener("removed", (_, machine) => {
     145            0 :             this.#remove_machine_frames(machine);
     146            0 :         });
     147          338 :         machines.addEventListener("added", (_, machine) => {
     148          338 :             this.#preload_machine_frames(machine);
     149          338 :         });
     150          338 :         machines.addEventListener("updated", (_, machine) => {
     151          338 :             if (!machine.visible || machine.problem)
     152           67 :                 this.#remove_machine_frames(machine);
     153              :             else
     154          338 :                 this.#preload_machine_frames(machine);
     155          338 :         });
     156              : 
     157          341 :         return machines;
     158          341 :     }
     159              : 
     160          341 :     #init_loader() {
     161          341 :         return machines_factory.loader(this.machines);
     162          341 :     }
     163              : 
     164              :     /* OOPS HANDLING
     165              :      */
     166              : 
     167          341 :     #init_oops() {
     168          341 :         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          341 :     }
     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          341 :     frames: { [name: string]: ShellFrame } = { };
     206              : 
     207          365 :     #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           99 :         if (machine.address != "localhost" && machine.state !== "connected")
     216           99 :             return null;
     217              : 
     218          365 :         const name = "cockpit1:" + machine.connection_string + "/" + path;
     219          365 :         let frame = this.frames[name];
     220              : 
     221          365 :         if (!frame) {
     222          365 :             frame = this.frames[name] = {
     223          365 :                 name,
     224          365 :                 host: machine.address,
     225          365 :                 path,
     226          365 :                 url: compute_frame_url(machine, path),
     227          365 :                 hash: hash || "/",
     228          365 :                 title,
     229          365 :                 ready: false,
     230          365 :                 loaded: false,
     231          365 :             };
     232          365 :         } else {
     233              :             // XXX - shouldn't we leave the hash alone when it is null here?
     234          365 :             frame.hash = hash || "/";
     235          365 :         }
     236          365 :         return frame;
     237          365 :     }
     238              : 
     239          338 :     ensure_frame_loaded (): void {
     240           63 :         if (this.current_frame && this.current_frame.url == null) {
     241              :             // Let update() recreate the frame.
     242           63 :             delete this.frames[this.current_frame.name];
     243           63 :             this.current_frame = null;
     244           63 :             this.update();
     245           63 :         }
     246          338 :     }
     247              : 
     248            6 :     #kill_frame(name: string): void {
     249              :         // Only mark frame as dead, it gets removed for real during
     250              :         // the call to "update".
     251            6 :         this.frames[name].url = null;
     252            6 :     }
     253              : 
     254            1 :     remove_frame (name: string): void {
     255            1 :         this.#kill_frame(name);
     256            1 :         this.update();
     257            1 :     }
     258              : 
     259            6 :     #remove_machine_frames (machine: Machine): void {
     260            6 :         const names = Object.keys(this.frames);
     261            6 :         for (const n of names) {
     262            6 :             if (this.frames[n].host == machine.address)
     263            6 :                 this.#kill_frame(n);
     264            6 :         }
     265            6 :         this.update();
     266            6 :     }
     267              : 
     268          338 :     #preload_machine_frames (machine: Machine) {
     269          338 :         const manifests = machine.manifests;
     270          338 :         const compiled = compile_manifests(manifests);
     271          338 :         for (const c in manifests) {
     272          338 :             const preload = manifests[c].preload as unknown as string[];
     273          329 :             if (preload && preload.length) {
     274          329 :                 for (const p of preload) {
     275           68 :                     const path = (p == "index") ? c : c + "/" + p;
     276          329 :                     const item = compiled.find_path_item(path);
     277          329 :                     this.#ensure_frame(machine, path, null, item.label);
     278          329 :                 }
     279          329 :             }
     280          338 :         }
     281          338 :         this.update();
     282          338 :     }
     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          341 :     page_status: { [host: string]: { [page: string]: Status } } = { };
     292              : 
     293          341 :     #init_page_status() {
     294          341 :         sessionStorage.removeItem("cockpit:page_status");
     295          341 :     }
     296              : 
     297           30 :     #notify_page_status(host: string, page: string, status: Status) {
     298           30 :         if (!this.page_status[host])
     299           30 :             this.page_status[host] = { };
     300           30 :         this.page_status[host][page] = status;
     301           30 :         sessionStorage.setItem("cockpit:page_status", JSON.stringify(this.page_status));
     302           30 :         this.update();
     303           30 :     }
     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          341 :     #init_router() {
     317          341 :         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          338 :             frame_is_initialized: (frame_name: string) => {
     325          338 :                 const frame = this.frames[frame_name];
     326          338 :                 if (frame) {
     327          338 :                     frame.loaded = true;
     328          338 :                     this.update();
     329          338 :                 }
     330          338 :                 this.#send_frame_hidden_hint(frame_name);
     331          338 :             },
     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           50 :             perform_frame_jump_command: (frame_name: string, location: string) => {
     342           38 :                 if (frame_name == "cockpit1" || (this.current_frame && this.current_frame.name == frame_name)) {
     343           50 :                     this.jump(location);
     344           50 :                     this.ensure_connection();
     345           50 :                 }
     346           50 :             },
     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          338 :             perform_frame_hash_track: (frame_name: string, hash: string) => {
     355              :                 /* Note that we ignore tracking for old shell code */
     356          338 :                 if (this.current_frame && this.current_frame.name === frame_name &&
     357          335 :                     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          335 :                     const location = Object.assign({}, decode_window_location(), { hash });
     363          335 :                     replace_window_location(location);
     364          335 :                     this.#remember_location(location.host, location.path, location.hash);
     365          335 :                     this.update();
     366          335 :                 }
     367          338 :             },
     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           30 :             handle_notifications: (host: string, page: string, data: { page_status?: Status }) => {
     376           30 :                 if (data.page_status !== undefined)
     377           30 :                     this.#notify_page_status(host, page, data.page_status);
     378           30 :             },
     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          341 :         };
     394              : 
     395          341 :         return new Router(callbacks);
     396          341 :     }
     397              : 
     398          338 :     #send_frame_hidden_hint (frame_name: string) {
     399          338 :         const hidden = !this.current_frame || this.current_frame.name != frame_name;
     400          338 :         this.router.hint(frame_name, { hidden });
     401          338 :     }
     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          341 :     #last_path_for_host: Record<string, string> = { };
     441          341 :     #last_hash_for_host_path: Record<string, Record<string, string>> = { };
     442              : 
     443           12 :     most_recent_path_for_host(host: string) {
     444           11 :         return this.#last_path_for_host[host] || "";
     445           12 :     }
     446              : 
     447           39 :     #most_recent_hash_for_path(host: string, path: string) {
     448           39 :         if (this.#last_hash_for_host_path[host])
     449           34 :             return this.#last_hash_for_host_path[host][path] || null;
     450           15 :         return null;
     451           39 :     }
     452              : 
     453          365 :     #remember_location(host: string, path: string, hash: string) {
     454          365 :         this.#last_path_for_host[host] = path;
     455          365 :         if (!this.#last_hash_for_host_path[host])
     456          365 :             this.#last_hash_for_host_path[host] = { };
     457          365 :         this.#last_hash_for_host_path[host][path] = hash;
     458          365 :     }
     459              : 
     460           58 :     jump (location: Partial<Location> | string): boolean {
     461           58 :         if (typeof location === "string")
     462           51 :             location = decode_location(location);
     463              : 
     464           58 :         const current = decode_window_location();
     465              : 
     466              :         /* Fill in the missing pieces, in order.
     467              :          */
     468              : 
     469           58 :         if (!location.host)
     470            5 :             location.host = current.host || "localhost";
     471              : 
     472           58 :         if (!location.path)
     473           15 :             location.path = this.most_recent_path_for_host(location.host);
     474              : 
     475           48 :         if (!location.hash) {
     476           41 :             if (location.host != current.host || location.path != current.path)
     477            8 :                 location.hash = this.#most_recent_hash_for_path(location.host, location.path) || "/";
     478              :             else
     479           17 :                 console.warn('Shell jump with hash and no frame change. Please use "/" as the hash to jump to the top sub-page.');
     480           48 :         }
     481              : 
     482           58 :         if (location.host !== current.host ||
     483           51 :             location.path !== current.path ||
     484           22 :             location.hash !== current.hash) {
     485           57 :             push_window_location(location);
     486           57 :             this.update();
     487           57 :             this.ensure_frame_loaded();
     488           57 :             return true;
     489           57 :         }
     490              : 
     491            9 :         this.ensure_frame_loaded();
     492            9 :         return false;
     493           58 :     }
     494              : 
     495          338 :     ensure_connection() {
     496          338 :         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          338 :             if (this.current_machine.connection_string == "localhost") {
     503          338 :                 this.loader.connect("localhost");
     504          338 :                 return;
     505          338 :             }
     506              : 
     507           69 :             this.emit("connect");
     508           69 :         }
     509          338 :     }
     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          341 :     current_location: Location | null = null;
     561          341 :     current_machine: Machine | null = null;
     562          341 :     current_manifest_item: ManifestItem | null = null;
     563          341 :     current_machine_manifest_items: CompiledComponents | null = null;
     564          341 :     current_manifest: Manifest | null = null;
     565              : 
     566          341 :     current_frame: ShellFrame | null = null;
     567              : 
     568          365 :     update() {
     569          365 :         if (!this.ready) {
     570          365 :             this.emit("update");
     571          365 :             return;
     572          365 :         }
     573              : 
     574          365 :         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          352 :         if (!this.config.host_switcher_enabled) {
     581          352 :             location.host = "localhost";
     582          352 :             replace_window_location(location);
     583          352 :         }
     584              : 
     585          365 :         let machine = this.machines.lookup(location.host);
     586              : 
     587              :         /* No such machine */
     588           96 :         if (!machine || !machine.visible) {
     589           96 :             machine = {
     590           96 :                 key: location.host,
     591           96 :                 connection_string: location.host,
     592           96 :                 address: location.host,
     593           96 :                 label: location.host,
     594           96 :                 state: "failed",
     595           96 :                 problem: "not-found",
     596           96 :             };
     597           96 :         }
     598              : 
     599          365 :         const compiled = compile_manifests(machine.manifests);
     600          114 :         if (machine.manifests && !location.path) {
     601              :             // Find the default path based on the manifest.
     602          114 :             const menu_items = compiled.ordered("menu");
     603          114 :             if (menu_items.length > 0 && menu_items[0])
     604           90 :                 location.path = menu_items[0].path;
     605              :             else
     606           90 :                 location.path = "system";
     607          114 :             replace_window_location(location);
     608          114 :         }
     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          365 :         this.#remember_location(location.host, location.path, location.hash);
     615              : 
     616          365 :         const item = compiled.find_path_item(location.path);
     617              : 
     618          365 :         this.current_location = location;
     619          365 :         this.current_machine = machine;
     620          365 :         this.current_machine_manifest_items = compiled;
     621          365 :         this.current_manifest_item = item;
     622          365 :         this.current_manifest = compiled.find_path_manifest(location.path);
     623              : 
     624          365 :         let frame = null;
     625          365 :         if (location.path && (machine.state == "connected" || machine.state == "connecting"))
     626          365 :             frame = this.#ensure_frame(machine, location.path, location.hash, item.label);
     627              : 
     628          365 :         if (frame != this.current_frame) {
     629          365 :             const prev_frame = this.current_frame;
     630          365 :             this.current_frame = frame;
     631              : 
     632          365 :             if (prev_frame)
     633          126 :                 this.#send_frame_hidden_hint(prev_frame.name);
     634          365 :             if (frame)
     635          365 :                 this.#send_frame_hidden_hint(frame.name);
     636          365 :         }
     637              : 
     638              :         // Remove all dead frames that are not the current one.
     639          365 :         for (const n of Object.keys(this.frames)) {
     640           95 :             if (this.frames[n].url == null && this.frames[n] != this.current_frame)
     641           95 :                 delete this.frames[n];
     642          365 :         }
     643              : 
     644          365 :         this.emit("update");
     645          365 :     }
     646          341 : }
        

Generated by: LCOV version 2.0-1