Skip to content

Commit

Permalink
Open path in terminal
Browse files Browse the repository at this point in the history
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 <acui@redhat.com>
  • Loading branch information
ashley-cui committed Nov 7, 2024
1 parent c295842 commit 5e2aaf5
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 32 deletions.
4 changes: 4 additions & 0 deletions pkg/systemd/overview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ class OverviewPage extends React.Component {
this.state.hostnameData.OperatingSystemPrettyName &&
<div className="ct-overview-header-subheading" id="system_information_os_text">{cockpit.format(_("running $0"), this.state.hostnameData.OperatingSystemPrettyName)}</div>}
</div>
{/* DEV ARTIFACT: FOR TESTING */}
<button ref={this.asdf}
className="pf-v5-c-button pf-m-secondary asdf"
onClick={() => cockpit.jump("/system/terminal#/?path=/home%2Fadmin%2Fa%2Fb")}>{_("asdf")}</button>
<div className='ct-overview-header-actions'>
{ show_superuser && <SuperuserIndicator proxy={this.superuser} /> }
{ "\n" }
Expand Down
106 changes: 102 additions & 4 deletions pkg/systemd/terminal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ 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 { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { fsinfo } from "cockpit/fsinfo";
import { Button } from '@patternfly/react-core';

import "./terminal.scss";

Expand All @@ -26,17 +29,20 @@ 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,
});
ch.addEventListener("ready", (_, msg) => this.setState({ pid: msg.pid }), { once: true });
ch.addEventListener("close", () => this.setState({ pid: null }), { once: true });
return ch;
}

constructor(props) {
Expand Down Expand Up @@ -66,12 +72,19 @@ const _ = cockpit.gettext;
title: 'Terminal',
theme: theme || "black-theme",
size: parseInt(size) || 16,
changePathBusy: false,
pathError: null,
pid: 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.forceChangeDirectory = this.forceChangeDirectory.bind(this);
this.onNavigate = this.onNavigate.bind(this);
this.dismissError = this.dismissError.bind(this);
this.dismissChange = this.dismissChange.bind(this);

this.terminalRef = React.createRef();
this.resetButtonRef = React.createRef();
Expand All @@ -81,8 +94,27 @@ const _ = cockpit.gettext;
}

async componentDidMount() {
cockpit.addEventListener("locationchanged", this.onNavigate);
const user = await cockpit.user();
this.setState({ user, channel: this.createChannel(user) });

let dir;
if (cockpit.location.options.path) {
try {
const info = await fsinfo(String(cockpit.location.options.path), ['type']);
if (info.type === "dir") {
dir = cockpit.location.options.path;
} else {
this.setState({ pathError: cockpit.format(_("$0 is not a directory"), cockpit.location.options.path) });
}
} catch (err) {
this.setState({ pathError: cockpit.format(_("$0 does not exist"), cockpit.location.options.path) });
}
}
this.setState({ user, channel: this.createChannel(user, dir) });
}

componentWillUnmount() {
cockpit.removeEventListener("locationchanged", this.onNavigate);
}

onTitleChanged(title) {
Expand All @@ -95,6 +127,56 @@ const _ = cockpit.gettext;
document.cookie = cookie;
}

forceChangeDirectory() {
this.setState(prevState => ({
channel: this.createChannel(prevState.user, cockpit.location.options.path),
changePathBusy: false,
}));
}

dismissError() {
this.setState({ pathError: null });
cockpit.location.replace("");
}

dismissChange() {
this.setState({ changePathBusy: false });
cockpit.location.replace("");
}

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
try {
const info = await fsinfo(String(cockpit.location.options.path), ['type']);
if (info.type !== "dir") {
this.setState({ pathError: cockpit.format(_("$0 is not a directory"), cockpit.location.options.path) });
return;
}
} catch (err) {
this.setState({ pathError: cockpit.format(_("$0 does not exist"), cockpit.location.options.path) });
return;
}

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(() => {
this.setState({ changePathBusy: true });
})
.catch(() => {
this.setState(prevState => ({ channel: this.createChannel(prevState.user, cockpit.location.options.path) }));
});
}
}

onPlus() {
this.setState((state, _) => {
localStorage.setItem('terminal:font-size', state.size + 1);
Expand Down Expand Up @@ -186,6 +268,22 @@ const _ = cockpit.gettext;
</ToolbarContent>
</Toolbar>
</div>
<div className="ct-terminal-dir-alert">
{this.state.pathError && <Alert isInline variant='danger'
title={cockpit.format(_("Error opening directory: $0"), this.state.pathError)}
actionClose={<AlertActionCloseButton onClose={this.dismissError} />} />}
{this.state.changePathBusy && <Alert title={_("There is still a process running in this terminal. Changing the directory will kill it.")}
variant="warning"
actionClose={<AlertActionCloseButton onClose={() =>
this.setState({ changePathBusy: false })} />}
actionLinks={
<>
<Button variant="secondary" size="sm" onClick={this.forceChangeDirectory}>{_("Continue")}</Button>
<AlertActionLink onClick={this.dismissChange}>{_("Cancel")}</AlertActionLink>
</>
} />
}
</div>
<div className={"terminal-body " + this.state.theme} id="the-terminal">
{terminal}
</div>
Expand Down
2 changes: 1 addition & 1 deletion pkg/systemd/terminal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading

0 comments on commit 5e2aaf5

Please sign in to comment.