From b3cb9a4b8e2da6c6ebe172418aa8b9122fb8054f Mon Sep 17 00:00:00 2001 From: Ashley Cui Date: Thu, 24 Oct 2024 15:51:54 -0400 Subject: [PATCH] Open path in terminal Allow opening of a path from a URL option, path=path. If the terminal is busy, then warn the user and prompt them to continue. Signed-off-by: Ashley Cui --- pkg/systemd/overview.jsx | 4 ++ pkg/systemd/terminal.jsx | 109 ++++++++++++++++++++++++++++++++++++-- pkg/systemd/terminal.scss | 2 +- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/pkg/systemd/overview.jsx b/pkg/systemd/overview.jsx index 7ade0dc27cb0..8300d8aabea8 100644 --- a/pkg/systemd/overview.jsx +++ b/pkg/systemd/overview.jsx @@ -168,6 +168,10 @@ class OverviewPage extends React.Component { this.state.hostnameData.OperatingSystemPrettyName &&
{cockpit.format(_("running $0"), this.state.hostnameData.OperatingSystemPrettyName)}
} + {/* DEV ARTIFACT: FOR TESTING */} +
{ show_superuser && } { "\n" } diff --git a/pkg/systemd/terminal.jsx b/pkg/systemd/terminal.jsx index 03ada383bae5..822298da5c7f 100644 --- a/pkg/systemd/terminal.jsx +++ b/pkg/systemd/terminal.jsx @@ -7,6 +7,11 @@ import { createRoot } from "react-dom/client"; import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js"; import { NumberInput } from "@patternfly/react-core/dist/esm/components/NumberInput/index.js"; import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js"; +import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js"; +import { Button } from '@patternfly/react-core/dist/esm/components/Button'; +import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; + + import "./terminal.scss"; @@ -26,17 +31,21 @@ const _ = cockpit.gettext; * Spawns the user's shell in the user's home directory. */ class UserTerminal extends React.Component { - createChannel(user) { - return cockpit.channel({ + createChannel(user, dir) { + const ch = cockpit.channel({ payload: "stream", spawn: [user.shell || "/bin/bash"], environ: [ "TERM=xterm-256color", ], - directory: user.home || "/", + directory: dir || user.home || "/", pty: true, binary: true, + pid: null, }); + ch.addEventListener("ready", (_, msg) => this.setState({ pid: msg.pid }), { once: true }); + ch.addEventListener("close", () => this.setState({ pid: null }), { once: true }); + return ch; } constructor(props) { @@ -66,12 +75,17 @@ const _ = cockpit.gettext; title: 'Terminal', theme: theme || "black-theme", size: parseInt(size) || 16, + changePathBusy: false, + pid: null, + pathError: null, }; this.onTitleChanged = this.onTitleChanged.bind(this); this.onResetClick = this.onResetClick.bind(this); this.onThemeChanged = this.onThemeChanged.bind(this); this.onPlus = this.onPlus.bind(this); this.onMinus = this.onMinus.bind(this); + this.onModal = this.onModal.bind(this); + this.onNavigate = this.onNavigate.bind(this); this.terminalRef = React.createRef(); this.resetButtonRef = React.createRef(); @@ -81,8 +95,35 @@ const _ = cockpit.gettext; } async componentDidMount() { + cockpit.addEventListener("locationchanged", this.onNavigate); const user = await cockpit.user(); - this.setState({ user, channel: this.createChannel(user) }); + var dir + if (cockpit.location.options.path) { + const exists = await this.dirExists(); + if (exists){ + dir = cockpit.location.options.path; + } else{ + this.setState({ pathError: cockpit.location.options.path }) + } + } + this.setState({ user, channel: this.createChannel(user, dir)}); + } + + async dirExists(){ + var exists + const cmmd = "test -d " + cockpit.location.options.path + await cockpit.script(cmmd, [], {err: "message"}) + .then(() => { + exists = true + }) + .catch((err) => { + exists = false + }) + return exists + } + + componentWillUnmount() { + cockpit.removeEventListener("locationchanged", this.onNavigate); } onTitleChanged(title) { @@ -95,6 +136,42 @@ const _ = cockpit.gettext; document.cookie = cookie; } + onModal(){ + this.setState({ changePathBusy: false }); + this.setState(prevState => ({ channel: this.createChannel(prevState.user, cockpit.location.options.path)})); + } + + async onNavigate(){ + // Clear old path errors + this.setState({ pathError: null }) + + // If there's no path to change to, then we're done here + if (!cockpit.location.options.path) { + return + } + + // Check if path we're changing to exists + const exists = await this.dirExists(); + if (!exists){ + // Show error and clear location option for path + this.setState({ pathError: cockpit.location.options.path }) + // cockpit.location.replace("") + } else if (this.state.pid !== null){ + // Check if current shell has a process running in it, ie it's busy + const cmmd = "grep -qr '^PPid:[[:space:]]*" + this.state.pid + "$' /proc/*/status"; + cockpit.script(cmmd, [], {err: "message"}) + .then(() => { + // it's busy: show a busy modal + this.setState({ changePathBusy: true }) + }) + .catch((err) => { + // it's not busy: change the path immediately + this.setState(prevState => ({ channel: this.createChannel(prevState.user, cockpit.location.options.path) })); + }) + } + + } + onPlus() { this.setState((state, _) => { localStorage.setItem('terminal:font-size', state.size + 1); @@ -186,6 +263,30 @@ const _ = cockpit.gettext;
+
+ {this.state.pathError && this.setState({ pathError: null })}> + }> + } + {this.state.changePathBusy && this.setState({ changePathBusy: false })}>} + actionLinks={ + + + Continue + + this.setState({ changePathBusy: false })} + > + Cancel + + + }> + + } +
{terminal}
diff --git a/pkg/systemd/terminal.scss b/pkg/systemd/terminal.scss index 0535f1c379ae..0e882dfb40d3 100644 --- a/pkg/systemd/terminal.scss +++ b/pkg/systemd/terminal.scss @@ -5,7 +5,7 @@ .console-ct-container { block-size: 100%; display: grid; - grid-template-rows: auto 1fr; + grid-template-rows: auto auto 1fr; overflow: hidden; }