Skip to content

Commit

Permalink
WIP - factor out router
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Sep 23, 2024
1 parent 13770f8 commit 24b1266
Show file tree
Hide file tree
Showing 3 changed files with 400 additions and 363 deletions.
304 changes: 3 additions & 301 deletions pkg/shell/base_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,310 +19,12 @@

import cockpit from "cockpit";

const shell_embedded = window.location.pathname.indexOf(".html") !== -1;
// const _ = cockpit.gettext;

function Router(index) {
const self = this;

let unique_id = 0;
const origin = cockpit.transport.origin;
const source_by_seed = { };
const source_by_name = { };

cockpit.transport.filter(function(message, channel, control) {
/* Only control messages with a channel are forwardable */
if (control) {
if (control.channel !== undefined) {
for (const seed in source_by_seed) {
const source = source_by_seed[seed];
if (!source.window.closed) {
// console.log("POST", source.window.name, JSON.stringify(message));
source.window.postMessage(message, origin);
}
}
// console.log("done");
} else if (control.command == "hint") {
/* This is where we handle hint messages directed at
* the shell. Right now, there aren't any.
*/
}

/* Forward message to relevant frame */
} else if (channel) {
const pos = channel.indexOf('!');
if (pos !== -1) {
const seed = channel.substring(0, pos + 1);
const source = source_by_seed[seed];
if (source) {
if (!source.window.closed)
source.window.postMessage(message, origin);
return false; /* Stop delivery */
}
}
}

/* Still deliver the message locally */
return true;
}, false);

function perform_jump(child, control) {
const current_frame = index.current_frame();
if (child !== window) {
if (!current_frame || current_frame.name != child.name)
return;
}
let str = control.location || "";
if (str[0] != "/")
str = "/" + str;
if (control.host)
str = "/@" + encodeURIComponent(control.host) + str;
index.jump(str);
}

function perform_track(child) {
const current_frame = index.current_frame();
/* Note that we ignore tracking for old shell code */
if (current_frame && current_frame.name === child.name &&
child.name && child.name.indexOf("/shell/shell") === -1) {
let hash = child.location.hash;
if (hash.indexOf("#") === 0)
hash = hash.substring(1);
if (hash === "/")
hash = "";
/* The browser has already pushed an appropriate entry to
the history, so let's just replace it with our custom
state object.
*/
const state = Object.assign({}, index.retrieve_state(), { hash });
index.navigate(state, true);
}
}

function on_unload(ev) {
let source;
if (ev.target.defaultView)
source = source_by_name[ev.target.defaultView.name];
else if (ev.view)
source = source_by_name[ev.view.name];
if (source)
unregister(source);
}

function on_hashchange(ev) {
const source = source_by_name[ev.target.name];
if (source)
perform_track(source.window);
}

function on_load(ev) {
const source = source_by_name[ev.target.contentWindow.name];
if (source)
perform_track(source.window);
}

function unregister(source) {
const child = source.window;
console.log("UNREGISTER", child.name);
cockpit.kill(null, child.name);
const frame = child.frameElement;
if (frame)
frame.removeEventListener("load", on_load);
/* This is often invalid when the window is closed */
if (child.removeEventListener) {
child.removeEventListener("unload", on_unload);
child.removeEventListener("hashchange", on_hashchange);
}
delete source_by_seed[source.channel_seed];
delete source_by_name[source.name];
}

function register(child) {
console.log("REGISTER", child.name);
let host, page;
const name = child.name || "";
if (name.indexOf("cockpit1:") === 0) {
const parts = name.substring(9).split("/");
host = parts[0];
page = parts.slice(1).join("/");
}
if (!name || !host || !page) {
console.warn("invalid child window name", child, name);
return;
}

unique_id += 1;
const seed = (cockpit.transport.options["channel-seed"] || "undefined:") + unique_id + "!";
const source = {
name,
window: child,
channel_seed: seed,
default_host: host,
page,
inited: false,
};
source_by_seed[seed] = source;
source_by_name[name] = source;

const frame = child.frameElement;
frame.addEventListener("load", on_load);
child.addEventListener("unload", on_unload);
child.addEventListener("hashchange", on_hashchange);

/*
* Setting the "data-loaded" attribute helps the testsuite
* know when it can switch into the frame and inject its
* own additions.
*/
frame.setAttribute('data-loaded', '1');

perform_track(child);

index.navigate();
return source;
}

function message_handler(event) {
if (event.origin !== origin)
return;

let data = event.data;
const child = event.source;
if (!child)
return;

/* If it's binary data just send it.
* TODO: Once we start restricting what frames can
* talk to which hosts, we need to parse control
* messages here, and cross check channels */
if (data instanceof window.ArrayBuffer) {
cockpit.transport.inject(data, true);
return;
}

if (typeof data !== "string")
return;

let source, control;

/*
* On Internet Explorer we see Access Denied when non Cockpit
* frames send messages (such as Javascript console). This also
* happens when the window is closed.
*/
try {
source = source_by_name[child.name];
} catch (ex) {
console.log("received message from child with in accessible name: ", ex);
return;
}
import { Router } from "./router.jsx";

/* Closing the transport */
if (data.length === 0) {
if (source)
unregister(source);
return;
}

/* A control message */
if (data[0] == '\n') {
control = JSON.parse(data.substring(1));
if (control.command === "init") {
if (source)
unregister(source);
if (control.problem) {
console.warn("child frame failed to init: " + control.problem);
source = null;
} else {
source = register(child);
}
if (source) {
const reply = {
...cockpit.transport.options,
command: "init",
host: source.default_host,
"channel-seed": source.channel_seed,
};
child.postMessage("\n" + JSON.stringify(reply), origin);
source.inited = true;

/* If this new frame is not the current one, tell it */
if (child.frameElement.name != index.current_frame()?.name)
self.hint(child.frameElement.name, { hidden: true });
}
} else if (control.command === "jump") {
perform_jump(child, control);
return;
} else if (control.command === "hint") {
if (control.hint == "restart") {
/* watchdog handles current host for now */
if (control.host != cockpit.transport.host)
index.expect_restart(control.host);
} else
cockpit.hint(control.hint, control);
return;
} else if (control.command == "oops") {
index.show_oops();
return;
} else if (control.command == "notify") {
if (source)
index.handle_notifications(source.default_host, source.page, control);
return;

/* Only control messages with a channel are forwardable */
} else if (control.channel === undefined && (control.command !== "logout" && control.command !== "kill")) {
return;

/* Add the child's group to all open channel messages */
} else if (control.command == "open") {
control.group = child.name;
data = "\n" + JSON.stringify(control);
}
}

if (!source) {
console.warn("child frame " + child.name + " sending data without init");
return;
}

/* Everything else gets forwarded */
cockpit.transport.inject(data, true);
}

self.start = function start(messages) {
window.addEventListener("message", message_handler, false);
for (let i = 0, len = messages.length; i < len; i++)
message_handler(messages[i]);
};

self.hint = function hint(name, data) {
const source = source_by_name[name];
/* This is often invalid when the window is closed */
if (source && source.inited && !source.window.closed) {
data.command = "hint";
const message = "\n" + JSON.stringify(data);
source.window.postMessage(message, origin);
}
};
const shell_embedded = window.location.pathname.indexOf(".html") !== -1;

self.unregister_name = (name) => {
const source = source_by_name[name];
if (source)
unregister(source);
else
console.warn("UNNOWN", name, source_by_name);
}
}
// const _ = cockpit.gettext;

/*
* New instances of Index must be created by new_index_from_proto
* and the caller must include a navigation function in the given
* prototype. That function will be called by Frames and
* Router to actually perform any navigation action.
*
* Emits "disconnect" and "expect_restart" signals, that should be
* handled by the caller.
*/
function Index() {
const self = this;
let current_frame;
Expand Down
Loading

0 comments on commit 24b1266

Please sign in to comment.