Skip to content

Commit

Permalink
systemd: 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.
  • Loading branch information
ashley-cui committed Nov 14, 2024
1 parent fb713ad commit 2f57d73
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 38 deletions.
116 changes: 112 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 }));
ch.addEventListener("close", () => this.setState({ pid: null }));
return ch;
}

constructor(props) {
Expand Down Expand Up @@ -66,12 +72,18 @@ 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.dismiss = this.dismiss.bind(this);

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

async componentDidMount() {
cockpit.addEventListener("locationchanged", this.onNavigate);

let dir;
if (cockpit.location.options.path) {
const validPath = await this.readyPath();
if (validPath) {
dir = cockpit.location.options.path;
}
}
const user = await cockpit.user();
this.setState({ user, channel: this.createChannel(user) });
this.setState({ user, channel: this.createChannel(user, dir) });
}

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

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

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

dismiss() {
this.setState({
pathError: null,
changePathBusy: false,
});
cockpit.location.replace("");
}

async onNavigate() {
// Clear old path errors
this.setState({
pathError: null,
changePathBusy: false,
});

// If there's no path to change to, then we're done here
if (!cockpit.location.options.path) {
return;
}
const changeNow = await this.readyPath();
if (changeNow) {
this.setState(prevState => ({ channel: this.createChannel(prevState.user, cockpit.location.options.path) }));
}
}

async readyPath() {
// 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 false;
}
} catch (err) {
this.setState({ pathError: cockpit.format(_("$0 does not exist"), cockpit.location.options.path) });
return false;
}

if (this.state.pid !== null) {
// Check if current shell has a process running in it, ie it's busy
const command = "grep -qr '^PPid:[[:space:]]*" + this.state.pid + "$' /proc/*/status";
try {
await cockpit.script(command, [], { err: "message" });
this.setState({ changePathBusy: true });
return false;
} catch {
return true;
}
}
return true;
}

onPlus() {
this.setState((state, _) => {
localStorage.setItem('terminal:font-size', state.size + 1);
Expand Down Expand Up @@ -186,6 +270,30 @@ const _ = cockpit.gettext;
</ToolbarContent>
</Toolbar>
</div>
<div className="ct-terminal-dir-alert">
{this.state.pathError && <Alert isInline
title={_("Error opening directory")}
variant="warning"
actionClose={<AlertActionCloseButton onClose={this.dismiss} />}>
<p>{_(this.state.pathError)}</p>
</Alert>
}

{this.state.changePathBusy && <Alert isInline
title={_("Change directory?")}
variant="danger"
actionClose={<AlertActionCloseButton onClose={() =>
this.setState({ changePathBusy: false })} />}
actionLinks={
<>
<Button variant="secondary" size="sm" onClick={this.forceChangeDirectory}>{_("Continue")}</Button>
<AlertActionLink onClick={this.dismiss}>{_("Cancel")}</AlertActionLink>
</>
}>
{_("There is still a process running in this terminal. Changing the directory will kill it.")}
</Alert>
}
</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 2f57d73

Please sign in to comment.