Skip to content

Commit

Permalink
WIP - idle timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Sep 23, 2024
1 parent 37c8aaa commit 3c98dcc
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 108 deletions.
85 changes: 2 additions & 83 deletions pkg/shell/base_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function component_checksum(machine, component) {
return "$" + machine.manifests[pkg][".checksum"];
}

function Frames(index, setupIdleResetTimers) {
function Frames(index) {
const self = this;
let language = document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1");
if (!language)
Expand Down Expand Up @@ -304,7 +304,6 @@ function Router(index) {
}
delete source_by_seed[source.channel_seed];
delete source_by_name[source.name];
console.log("SOURCES", source_by_seed);
}

function register(child) {
Expand Down Expand Up @@ -348,8 +347,6 @@ function Router(index) {

perform_track(child);

console.log("SOURCES", source_by_seed, source_by_name);

index.navigate();
return source;
}
Expand Down Expand Up @@ -504,87 +501,9 @@ function Index() {
if (typeof self.navigate !== "function")
throw Error("Index requires a prototype with a navigate function");

/* Session timing out after inactivity */
let session_final_timer = null;
let session_timeout = 0;
let current_idle_time = 0;
let final_countdown = 30000; // last 30 seconds
let title = "";
const standard_login = window.localStorage['standard-login'];

self.has_oops = false;

function sessionTimeout() {
current_idle_time += 5000;
if (!session_final_timer && current_idle_time >= session_timeout - final_countdown) {
title = document.title;
sessionFinalTimeout();
}
}

let session_timeout_dialog_root = null;

/* XXX
function updateFinalCountdown() {
const remaining_secs = Math.floor(final_countdown / 1000);
const timeout_text = cockpit.format(_("You will be logged out in $0 seconds."), remaining_secs);
document.title = "(" + remaining_secs + ") " + title;
if (!session_timeout_dialog_root)
session_timeout_dialog_root = createRoot(document.getElementById('session-timeout-dialog'));
session_timeout_dialog_root.render(React.createElement(TimeoutModal, {
onClose: () => {
window.clearTimeout(session_final_timer);
session_final_timer = null;
document.title = title;
resetTimer();
session_timeout_dialog_root.unmount();
session_timeout_dialog_root = null;
final_countdown = 30000;
},
text: timeout_text,
}));
}
function sessionFinalTimeout() {
final_countdown -= 1000;
if (final_countdown > 0) {
updateFinalCountdown();
session_final_timer = window.setTimeout(sessionFinalTimeout, 1000);
} else {
cockpit.logout(true, _("You have been logged out due to inactivity."));
}
}
*/

/* Auto-logout idle timer */
function resetTimer(ev) {
if (!session_final_timer) {
current_idle_time = 0;
}
}

function setupIdleResetTimers(win) {
win.addEventListener("mousemove", resetTimer, false);
win.addEventListener("mousedown", resetTimer, false);
win.addEventListener("keypress", resetTimer, false);
win.addEventListener("touchmove", resetTimer, false);
win.addEventListener("scroll", resetTimer, false);
}

cockpit.dbus(null, { bus: "internal" }).call("/config", "cockpit.Config", "GetUInt", ["Session", "IdleTimeout", 0, 240, 0], [])
.then(result => {
session_timeout = result[0] * 60000;
if (session_timeout > 0 && standard_login) {
setupIdleResetTimers(window);
window.setInterval(sessionTimeout, 5000);
}
})
.catch(e => {
if (e.message.indexOf("GetUInt not available") === -1)
console.warn(e.message);
});

self.frames = new Frames(self, setupIdleResetTimers);
self.frames = new Frames(self);
self.router = new Router(self);

/* Watchdog for disconnect */
Expand Down
21 changes: 11 additions & 10 deletions pkg/shell/frames.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,22 @@

import React, { useRef, useEffect } from 'react';

function poll_frame_ready(state, frame, elt, count) {
function poll_frame_ready(state, frame, elt, count, setupFrameWindow) {
let ready = false;

try {
if (elt.contentWindow.document && elt.contentWindow.document.body)
ready = elt.contentWindow.document.body.offsetWidth > 0 && elt.contentWindow.document.body.offsetHeight > 0;
if (elt.contentWindow.document && elt.contentWindow.document.body) {
ready = (elt.contentWindow.location.href != "about:blank" &&
elt.contentWindow.document.body.offsetWidth > 0 &&
elt.contentWindow.document.body.offsetHeight > 0);
}
} catch (ex) {
ready = true;
}

if (!count)
count = 0;

console.log("POLL", elt.name, ready, count);

count += 1;
if (count > 50)
ready = true;
Expand All @@ -77,8 +78,8 @@ function poll_frame_ready(state, frame, elt, count) {
elt.setAttribute("data-ready", "1");
}

// if (elt.contentWindow && setupIdleResetTimers)
// setupIdleResetTimers(elt.contentWindow);
if (elt.contentWindow && setupFrameWindow)
setupFrameWindow(elt.contentWindow);

// if (elt.contentDocument && elt.contentDocument.documentElement) {
// elt.contentDocument.documentElement.lang = language;
Expand All @@ -88,12 +89,12 @@ function poll_frame_ready(state, frame, elt, count) {
} else {
elt.removeAttribute("data-ready");
window.setTimeout(function() {
poll_frame_ready(state, frame, elt, count + 1);
poll_frame_ready(state, frame, elt, count + 1, setupFrameWindow);
}, 100);
}
}

export const Frames = ({ state, current_frame }) => {
export const Frames = ({ state, current_frame, setupFrameWindow }) => {
const content_ref = useRef(null);
const { frames } = state;

Expand Down Expand Up @@ -194,7 +195,7 @@ export const Frames = ({ state, current_frame }) => {
iframe.style.display = (frame == current_frame) ? "block" : "none";

if (need_poll)
poll_frame_ready(state, frame, iframe);
poll_frame_ready(state, frame, iframe, 0, setupFrameWindow);

// XXX - doesn't work (or is not enough), document has
// zero height. Make content-area dark?
Expand Down
121 changes: 121 additions & 0 deletions pkg/shell/idle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2024 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <https://www.gnu.org/licenses/>.
*/

/* Session timing out after inactivity */

import cockpit from "cockpit";

import React from 'react';
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";

const _ = cockpit.gettext;

export const FinalCountdownModal = ({ state }) => {
if (state.final_countdown === false)
return null;

return (
<Modal isOpen position="top" variant="medium"
showClose={false}
title={_("Session is about to expire")}
id="session-timeout-modal"
footer={<Button variant='primary'
onClick={() => state.cancel_final_countdown()}>
{_("Continue session")}
</Button>} >
{ cockpit.format(_("You will be logged out in $0 seconds."), state.final_countdown) }
</Modal>
);
}

export const IdleTimeoutState = () => {
const final_countdown_secs = 30;
const standard_login = window.localStorage['standard-login'];

let final_countdown_timer = -1;
let session_timeout = 0;
let current_idle_time = 0;

const self = {
final_countdown: false,
};

function update() {
self.dispatchEvent("update");
}

cockpit.event_target(self);

function idleTick() {
current_idle_time += 5000;
if (self.final_countdown === false && current_idle_time >= session_timeout - final_countdown_secs * 1000) {
// It's the final countdown...
self.final_countdown = final_countdown_secs;
final_countdown_timer = window.setInterval(finalCountdownTick, 1000);
update();
}
}

function finalCountdownTick() {
self.final_countdown -= 1;
if (self.final_countdown <= 0)
cockpit.logout(true, _("You have been logged out due to inactivity."));
update();
}

function resetTimer(ev) {
if (self.final_countdown === false)
current_idle_time = 0;
}

function setupIdleResetEventListeners(win) {
if (session_timeout > 0 && standard_login) {
win.addEventListener("mousemove", resetTimer, false);
win.addEventListener("mousedown", resetTimer, false);
win.addEventListener("keypress", resetTimer, false);
win.addEventListener("touchmove", resetTimer, false);
win.addEventListener("scroll", resetTimer, false);
}
}

self.setupIdleResetEventListeners = setupIdleResetEventListeners;

cockpit.dbus(null, { bus: "internal" }).call("/config", "cockpit.Config", "GetUInt", ["Session", "IdleTimeout", 0, 240, 0], [])
.then(result => {
session_timeout = result[0] * 60000;
if (session_timeout > 0 && standard_login) {
setupIdleResetEventListeners(window);
window.setInterval(idleTick, 5000);
}
})
.catch(e => {
if (e.message.indexOf("GetUInt not available") === -1)
console.warn(e.message);
});

self.cancel_final_countdown = function () {
current_idle_time = 0;
self.final_countdown = false;
window.clearInterval(final_countdown_timer);
update();
};

return self;
}
13 changes: 0 additions & 13 deletions pkg/shell/shell-modals.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,19 +167,6 @@ export const LangModal = ({ dialogResult }) => {
);
};

export function TimeoutModal(props) {
return (
<Modal isOpen position="top" variant="medium"
showClose={false}
title={_("Session is about to expire")}
id="session-timeout-modal"
footer={<Button variant='primary' onClick={props.onClose}>{_("Continue session")}</Button>}
>
{props.text}
</Modal>
);
}

export function OopsModal({ dialogResult }) {
return (
<Modal isOpen position="top" variant="medium"
Expand Down
15 changes: 13 additions & 2 deletions pkg/shell/shell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import cockpit from "cockpit";
import React from 'react';
import { createRoot } from "react-dom/client";

import { WithDialogs } from "dialogs.jsx";
import { WithDialogs, useDialogs } from "dialogs.jsx";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import useDialogs.
import { useInit, useEvent, useLoggedInUser } from "hooks";

import { TopNav } from "./topnav.jsx";
Expand All @@ -32,6 +32,7 @@ import { Frames } from "./frames.jsx";
import { EarlyFailure, EarlyFailureReady, MachineTroubleshoot } from "./failures.jsx";

import { ShellState } from "./state.jsx";
import { IdleTimeoutState, FinalCountdownModal } from "./idle.jsx";

import 'cockpit-dark-theme'; // once per page

Expand Down Expand Up @@ -93,6 +94,9 @@ const Shell = () => {
useEvent(state, "update");
const current_user = useLoggedInUser()?.name || "";

const idle_state = useInit(() => IdleTimeoutState());
useEvent(idle_state, "update");

/* machine - Current machine
component - Current component from a manifest, such as "playground" or "system/logs"
fullpath - Current full path (without hash) in that component, such as "playground/test"
Expand Down Expand Up @@ -138,6 +142,9 @@ const Shell = () => {
title_parts.push((machine.user || current_user) + "@" + machine.label);
document.title = title_parts.join(" - ");

if (idle_state.final_countdown)
document.title = "(" + idle_state.final_countdown + ") " + document.title;

let failure = null;
if (problem) {
failure = (
Expand Down Expand Up @@ -203,14 +210,18 @@ const Shell = () => {
compiled={compiled} />
</div>

<Frames state={state} current_frame={current_frame} />
<Frames state={state}
current_frame={current_frame}
setupFrameWindow={win => idle_state.setupIdleResetEventListeners(win)} />

{ failure &&
<div id="failure-content" className="area-ct-content" role="main" tabIndex="-1">
{ failure }
</div>
}

<FinalCountdownModal state={idle_state} />

</div>);
};

Expand Down
2 changes: 2 additions & 0 deletions test/verify/check-shell-menu
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ class TestMenu(testlib.MachineCase):
b = self.browser
m = self.machine

m.upload(["/home/mvo/work/cockpit/dist/shell"], "/usr/share/cockpit")

m.execute("printf '[Session]\nIdleTimeout = 1\n' >> /etc/cockpit/cockpit.conf")

# does not time out immediately
Expand Down

0 comments on commit 3c98dcc

Please sign in to comment.