Add runtime-configured Lab 3 browser terminal
This commit is contained in:
@@ -20,20 +20,22 @@ npm run dev
|
|||||||
|
|
||||||
## Lab 3 Web Terminal
|
## Lab 3 Web Terminal
|
||||||
|
|
||||||
Set `NEXT_PUBLIC_LAB3_TERMINAL_PATH` to the WeTTY endpoint used by your deployment. The default expected path is `/wetty`, and `.env.example` includes that value. Local environments can also provide a full URL such as `http://127.0.0.1:7681/wetty`.
|
The Lab 3 terminal now prefers a runtime-served config artifact at `/courseware-runtime.json`.
|
||||||
|
|
||||||
The Lab 3 widget assumes:
|
Expected fields:
|
||||||
|
|
||||||
- WeTTY runs on the lab host and is bound to `127.0.0.1`
|
- `lab3TerminalUrl`
|
||||||
- the public proxy forwards `/wetty` to the local WeTTY port
|
- `lab3Username`
|
||||||
- WebSocket upgrade happens at the reverse proxy
|
- `lab3WorkingDirectory`
|
||||||
- WeTTY is launched as root so it can present `/bin/login` locally instead of SSH
|
|
||||||
|
|
||||||
Example service command:
|
Local development still falls back to `/wetty`, and `.env.example` keeps that default for simple standalone runs.
|
||||||
|
|
||||||
```bash
|
The Linux/WSL deployment contract is:
|
||||||
wetty --host 127.0.0.1 --port 3001 --base /wetty --allow-iframe
|
|
||||||
```
|
- loopback-only `sshd` on `127.0.0.1:22`
|
||||||
|
- WeTTY exposed to the browser on `http://127.0.0.1:7681/wetty`
|
||||||
|
- a managed `student` login
|
||||||
|
- a working directory at `/home/student/lab3`
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ In this lab, we will:
|
|||||||
<strong>Execute</strong> sections require running commands and producing output.
|
<strong>Execute</strong> sections require running commands and producing output.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
To start this lab, use the embedded terminal below. It connects to the same lab machine in your browser and should prompt you to log in with the default `student` account.
|
To start this lab, use the embedded terminal below. It connects to the same lab machine in your browser and should prompt you to log in with the managed `student` account.
|
||||||
|
|
||||||
<div data-lab3-terminal></div>
|
<div data-lab3-terminal></div>
|
||||||
|
|
||||||
If the embedded terminal is unavailable, you can still fall back to:
|
If the embedded terminal is unavailable, you can still fall back to:
|
||||||
|
|
||||||
- SSH - <IP>:22
|
- SSH - <IP>:22
|
||||||
- All necessary artifacts are in the `lab3` folder
|
- The lab workspace is rooted at `/home/student/lab3`
|
||||||
|
|
||||||
## Objective 1: HuggingFace & LLaMa.cpp
|
## Objective 1: HuggingFace & LLaMa.cpp
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ The project’s original goal was to make LLaMA models accessible on systems wit
|
|||||||
For this lab we will work with **WhiteRabbitNeo‑V3‑7B**, a cybersecurity‑oriented fine‑tune of Qwen2.5‑Coder‑7B. This model is less popular than LLaMA-3.2, and if we'd like to run it in `llama.cpp` or Ollama, we first need to convert it into a usable GGUF artifact.
|
For this lab we will work with **WhiteRabbitNeo‑V3‑7B**, a cybersecurity‑oriented fine‑tune of Qwen2.5‑Coder‑7B. This model is less popular than LLaMA-3.2, and if we'd like to run it in `llama.cpp` or Ollama, we first need to convert it into a usable GGUF artifact.
|
||||||
|
|
||||||
<div class="lab-callout lab-callout--warning">
|
<div class="lab-callout lab-callout--warning">
|
||||||
<strong>Warning:</strong> Although the next two steps show how to find and download this model so you can replicate the process, support files are already provided in <code>/home/student/lab3/WhiteRabbitNeo</code> to speed up lab execution.
|
<strong>Warning:</strong> Although the next two steps show how to find and download this model so you can replicate the process, any course-provided WhiteRabbitNeo support files will be staged under <code>/home/student/lab3/WhiteRabbitNeo</code> when they are available in the deployment.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### 1. Locate & download the model
|
### 1. Locate & download the model
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
# Lab 3 Embedded Terminal Deployment
|
# Lab 3 Embedded Terminal Deployment
|
||||||
|
|
||||||
Lab 3 now expects a same-origin WeTTY endpoint mounted at `/wetty` by default.
|
Lab 3 now prefers a runtime config artifact served at `/courseware-runtime.json`.
|
||||||
|
|
||||||
|
Default runtime config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lab3TerminalUrl": "http://127.0.0.1:7681/wetty",
|
||||||
|
"lab3Username": "student",
|
||||||
|
"lab3WorkingDirectory": "/home/student/lab3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Runtime Contract
|
## Runtime Contract
|
||||||
|
|
||||||
- Run WeTTY on the lab host, bound to `127.0.0.1`
|
- Create a managed `student` Unix account
|
||||||
- Mount it behind the same public origin as the Next.js app
|
- Bind `sshd` to `127.0.0.1:22` only
|
||||||
- Let the reverse proxy handle WebSocket upgrade for `/wetty`
|
- Run WeTTY on the lab host and expose it to the browser at `/wetty`
|
||||||
- Launch WeTTY as root so it uses `/bin/login` locally
|
- Point WeTTY at localhost SSH without pre-setting the username so students see a login prompt
|
||||||
- Keep SSH available as a fallback path, not the primary student UX
|
- Start students in `/home/student/lab3`
|
||||||
|
|
||||||
Example launch command:
|
Example launch command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wetty --host 127.0.0.1 --port 3001 --base /wetty --allow-iframe
|
wetty --host 0.0.0.0 --port 7681 --base /wetty --allow-iframe --ssh-host 127.0.0.1 --ssh-port 22 --ssh-auth password
|
||||||
```
|
```
|
||||||
|
|
||||||
## Proxmox VM Shape
|
## Linux / WSL Shape
|
||||||
|
|
||||||
- Install WeTTY as a system service
|
- Install `openssh-server`
|
||||||
- Keep it bound to localhost only
|
- Restrict `sshd` to `127.0.0.1` and the managed `student` account
|
||||||
- Reverse proxy `/wetty` to the local WeTTY port over HTTPS
|
- Install WeTTY with the managed Node runtime
|
||||||
|
- Generate `/courseware-runtime.json` from deployment-time values
|
||||||
## Docker Shape
|
- Start the wiki and WeTTY as separate managed services
|
||||||
|
|
||||||
- Include WeTTY in the lab image
|
|
||||||
- Include the `login` and PAM pieces needed for `/bin/login`
|
|
||||||
- Run WeTTY under an init or supervisor process
|
|
||||||
- Reverse proxy `/wetty` to the local WeTTY port over HTTPS
|
|
||||||
|
|
||||||
If a specific container image cannot support `/bin/login`, the operational fallback is WeTTY SSH mode to localhost.
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
Lab3TerminalFrame,
|
||||||
|
} from "~/components/labs/Lab3TerminalFrame";
|
||||||
import {
|
import {
|
||||||
LAB3_DEFAULT_TERMINAL_PATH,
|
LAB3_DEFAULT_TERMINAL_PATH,
|
||||||
Lab3TerminalFrame,
|
LAB3_DEFAULT_WORKING_DIRECTORY,
|
||||||
|
fetchLab3RuntimeConfig,
|
||||||
getLab3TerminalPath,
|
getLab3TerminalPath,
|
||||||
} from "~/components/labs/Lab3TerminalFrame";
|
LAB3_RUNTIME_CONFIG_PATH,
|
||||||
|
normalizeLab3RuntimeConfig,
|
||||||
|
} from "~/lib/lab3-runtime";
|
||||||
|
|
||||||
describe("getLab3TerminalPath", () => {
|
describe("getLab3TerminalPath", () => {
|
||||||
it("falls back to the default same-origin terminal path", () => {
|
it("falls back to the default same-origin terminal path", () => {
|
||||||
@@ -20,17 +26,54 @@ describe("getLab3TerminalPath", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Lab3TerminalFrame", () => {
|
describe("normalizeLab3RuntimeConfig", () => {
|
||||||
const originalEnvValue = process.env.NEXT_PUBLIC_LAB3_TERMINAL_PATH;
|
it("fills in the default student terminal metadata", () => {
|
||||||
|
expect(normalizeLab3RuntimeConfig()).toEqual({
|
||||||
afterEach(() => {
|
terminalPath: LAB3_DEFAULT_TERMINAL_PATH,
|
||||||
process.env.NEXT_PUBLIC_LAB3_TERMINAL_PATH = originalEnvValue;
|
username: "student",
|
||||||
|
workingDirectory: LAB3_DEFAULT_WORKING_DIRECTORY,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the embedded terminal with the configured path", () => {
|
describe("fetchLab3RuntimeConfig", () => {
|
||||||
process.env.NEXT_PUBLIC_LAB3_TERMINAL_PATH = "lab-terminal";
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
render(<Lab3TerminalFrame />);
|
it("reads the runtime config artifact from the public path", async () => {
|
||||||
|
const fetchMock = vi
|
||||||
|
.spyOn(globalThis, "fetch")
|
||||||
|
.mockResolvedValue(
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
lab3TerminalUrl: "http://127.0.0.1:7681/wetty",
|
||||||
|
lab3Username: "student",
|
||||||
|
lab3WorkingDirectory: "/home/student/lab3",
|
||||||
|
}),
|
||||||
|
{ status: 200 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(fetchLab3RuntimeConfig()).resolves.toEqual({
|
||||||
|
terminalPath: "http://127.0.0.1:7681/wetty",
|
||||||
|
username: "student",
|
||||||
|
workingDirectory: "/home/student/lab3",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(LAB3_RUNTIME_CONFIG_PATH, {
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Lab3TerminalFrame", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the embedded terminal with the configured path override", () => {
|
||||||
|
render(<Lab3TerminalFrame srcPath="lab-terminal" />);
|
||||||
|
|
||||||
const frame = screen.getByTitle("Lab 3 terminal session");
|
const frame = screen.getByTitle("Lab 3 terminal session");
|
||||||
expect(frame).toHaveAttribute("src", "/lab-terminal");
|
expect(frame).toHaveAttribute("src", "/lab-terminal");
|
||||||
@@ -42,10 +85,36 @@ describe("Lab3TerminalFrame", () => {
|
|||||||
expect(screen.getAllByText("/home/student/lab3")).toHaveLength(2);
|
expect(screen.getAllByText("/home/student/lab3")).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads the terminal settings from the runtime config artifact", async () => {
|
||||||
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
lab3TerminalUrl: "http://127.0.0.1:7681/wetty",
|
||||||
|
lab3Username: "student",
|
||||||
|
lab3WorkingDirectory: "/srv/labs/lab3",
|
||||||
|
}),
|
||||||
|
{ status: 200 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Lab3TerminalFrame />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTitle("Lab 3 terminal session")).toHaveAttribute(
|
||||||
|
"src",
|
||||||
|
"http://127.0.0.1:7681/wetty",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getAllByText("/srv/labs/lab3")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
it("toggles the dock open and closed", () => {
|
it("toggles the dock open and closed", () => {
|
||||||
render(<Lab3TerminalFrame srcPath="/wetty" />);
|
render(<Lab3TerminalFrame srcPath="/wetty" />);
|
||||||
|
|
||||||
const toggle = screen.getAllByRole("button", { name: /open terminal|hide terminal|lab 3 terminal/i })[0];
|
const toggle = screen.getAllByRole("button", {
|
||||||
|
name: /open terminal|hide terminal|lab 3 terminal/i,
|
||||||
|
})[0];
|
||||||
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||||
|
|
||||||
fireEvent.click(toggle);
|
fireEvent.click(toggle);
|
||||||
|
|||||||
@@ -1,34 +1,60 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
export const LAB3_DEFAULT_TERMINAL_PATH = "/wetty";
|
import { fetchLab3RuntimeConfig, normalizeLab3RuntimeConfig } from "~/lib/lab3-runtime";
|
||||||
|
|
||||||
export function getLab3TerminalPath(envValue?: string) {
|
|
||||||
const trimmedValue = envValue?.trim();
|
|
||||||
|
|
||||||
if (!trimmedValue) {
|
|
||||||
return LAB3_DEFAULT_TERMINAL_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^https?:\/\//i.test(trimmedValue)) {
|
|
||||||
return trimmedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmedValue.startsWith("/") ? trimmedValue : `/${trimmedValue}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Lab3TerminalFrameProps = {
|
type Lab3TerminalFrameProps = {
|
||||||
srcPath?: string;
|
srcPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Lab3TerminalFrame({
|
export function Lab3TerminalFrame({ srcPath }: Lab3TerminalFrameProps) {
|
||||||
srcPath = getLab3TerminalPath(process.env.NEXT_PUBLIC_LAB3_TERMINAL_PATH),
|
|
||||||
}: Lab3TerminalFrameProps) {
|
|
||||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
|
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
|
||||||
const terminalPath = useMemo(() => getLab3TerminalPath(srcPath), [srcPath]);
|
const [isConfigResolved, setIsConfigResolved] = useState(Boolean(srcPath));
|
||||||
|
const [runtimeConfig, setRuntimeConfig] = useState(() => {
|
||||||
|
return normalizeLab3RuntimeConfig(
|
||||||
|
srcPath ? { lab3TerminalUrl: srcPath } : undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const terminalPath = runtimeConfig.terminalPath;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
|
if (srcPath) {
|
||||||
|
setRuntimeConfig(normalizeLab3RuntimeConfig({ lab3TerminalUrl: srcPath }));
|
||||||
|
setIsConfigResolved(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConfigResolved(false);
|
||||||
|
setStatus("loading");
|
||||||
|
|
||||||
|
void fetchLab3RuntimeConfig()
|
||||||
|
.then((nextConfig) => {
|
||||||
|
if (isCancelled) return;
|
||||||
|
setRuntimeConfig(nextConfig);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (isCancelled) return;
|
||||||
|
setRuntimeConfig(normalizeLab3RuntimeConfig());
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (isCancelled) return;
|
||||||
|
setIsConfigResolved(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
|
}, [srcPath]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStatus("loading");
|
||||||
|
}, [terminalPath]);
|
||||||
|
|
||||||
function handleFrameLoad() {
|
function handleFrameLoad() {
|
||||||
const frame = frameRef.current;
|
const frame = frameRef.current;
|
||||||
@@ -61,7 +87,8 @@ export function Lab3TerminalFrame({
|
|||||||
<h3>Use the lab shell directly in your browser</h3>
|
<h3>Use the lab shell directly in your browser</h3>
|
||||||
<p className="lab3-terminal-inline__lede">
|
<p className="lab3-terminal-inline__lede">
|
||||||
The terminal is docked to the bottom of the page. Expand it with the
|
The terminal is docked to the bottom of the page. Expand it with the
|
||||||
arrow when you're ready to work from <code>/home/student/lab3</code>.
|
arrow when you're ready to log in as <code>{runtimeConfig.username}</code>{" "}
|
||||||
|
and work from <code>{runtimeConfig.workingDirectory}</code>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -76,6 +103,7 @@ export function Lab3TerminalFrame({
|
|||||||
{isOpen ? "Hide Terminal" : "Open Terminal"}
|
{isOpen ? "Hide Terminal" : "Open Terminal"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{isConfigResolved ? (
|
||||||
<a
|
<a
|
||||||
className="lab3-terminal-inline__link"
|
className="lab3-terminal-inline__link"
|
||||||
href={terminalPath}
|
href={terminalPath}
|
||||||
@@ -84,6 +112,11 @@ export function Lab3TerminalFrame({
|
|||||||
>
|
>
|
||||||
Open in New Tab
|
Open in New Tab
|
||||||
</a>
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="lab3-terminal-inline__link" aria-live="polite">
|
||||||
|
Loading terminal...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -107,10 +140,12 @@ export function Lab3TerminalFrame({
|
|||||||
<div className="lab3-terminal-dock__panel" id="lab3-terminal-dock-panel">
|
<div className="lab3-terminal-dock__panel" id="lab3-terminal-dock-panel">
|
||||||
<div className="lab3-terminal-dock__header">
|
<div className="lab3-terminal-dock__header">
|
||||||
<p className="lab3-terminal-dock__lede">
|
<p className="lab3-terminal-dock__lede">
|
||||||
Work from <code>/home/student/lab3</code>, or pop the terminal out
|
Log in as <code>{runtimeConfig.username}</code>, work from{" "}
|
||||||
into a full tab for more room.
|
<code>{runtimeConfig.workingDirectory}</code>, or pop the terminal
|
||||||
|
out into a full tab for more room.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{isConfigResolved ? (
|
||||||
<a
|
<a
|
||||||
className="lab3-terminal-dock__link"
|
className="lab3-terminal-dock__link"
|
||||||
href={terminalPath}
|
href={terminalPath}
|
||||||
@@ -119,9 +154,11 @@ export function Lab3TerminalFrame({
|
|||||||
>
|
>
|
||||||
Open in New Tab
|
Open in New Tab
|
||||||
</a>
|
</a>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lab3-terminal-dock__frame-shell">
|
<div className="lab3-terminal-dock__frame-shell">
|
||||||
|
{isConfigResolved ? (
|
||||||
<iframe
|
<iframe
|
||||||
className="lab3-terminal-dock__frame"
|
className="lab3-terminal-dock__frame"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -131,11 +168,16 @@ export function Lab3TerminalFrame({
|
|||||||
src={terminalPath}
|
src={terminalPath}
|
||||||
title="Lab 3 terminal session"
|
title="Lab 3 terminal session"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="lab3-terminal-dock__frame" aria-hidden="true" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status === "loading" ? (
|
{status === "loading" ? (
|
||||||
<p className="lab3-terminal-dock__status" aria-live="polite">
|
<p className="lab3-terminal-dock__status" aria-live="polite">
|
||||||
Connecting to the embedded terminal...
|
{isConfigResolved
|
||||||
|
? "Connecting to the embedded terminal..."
|
||||||
|
: "Loading terminal configuration..."}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -144,12 +186,18 @@ export function Lab3TerminalFrame({
|
|||||||
className="lab3-terminal-dock__status lab3-terminal-dock__status--warning"
|
className="lab3-terminal-dock__status lab3-terminal-dock__status--warning"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
The reverse proxy is not running for <code>{terminalPath}</code>.
|
The browser terminal service is unavailable right now.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="lab3-terminal-dock__status">
|
<p className="lab3-terminal-dock__status">
|
||||||
If the terminal page does not appear, open it in a new tab or fall
|
{isConfigResolved ? (
|
||||||
back to SSH on <code><IP>:22</code>.
|
<>
|
||||||
|
If the terminal page does not appear, open it in a new tab and
|
||||||
|
confirm the service is reachable at <code>{terminalPath}</code>.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Waiting for terminal settings from the deployment runtime."
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export const LAB3_DEFAULT_TERMINAL_PATH = "/wetty";
|
||||||
|
export const LAB3_DEFAULT_USERNAME = "student";
|
||||||
|
export const LAB3_DEFAULT_WORKING_DIRECTORY = "/home/student/lab3";
|
||||||
|
export const LAB3_RUNTIME_CONFIG_PATH = "/courseware-runtime.json";
|
||||||
|
|
||||||
|
export type Lab3RuntimeConfig = {
|
||||||
|
lab3TerminalUrl?: string;
|
||||||
|
lab3Username?: string;
|
||||||
|
lab3WorkingDirectory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResolvedLab3RuntimeConfig = {
|
||||||
|
terminalPath: string;
|
||||||
|
username: string;
|
||||||
|
workingDirectory: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLab3TerminalPath(envValue?: string) {
|
||||||
|
const trimmedValue = envValue?.trim();
|
||||||
|
|
||||||
|
if (!trimmedValue) {
|
||||||
|
return LAB3_DEFAULT_TERMINAL_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^https?:\/\//i.test(trimmedValue)) {
|
||||||
|
return trimmedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedValue.startsWith("/") ? trimmedValue : `/${trimmedValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeLab3RuntimeConfig(
|
||||||
|
config?: Lab3RuntimeConfig,
|
||||||
|
): ResolvedLab3RuntimeConfig {
|
||||||
|
return {
|
||||||
|
terminalPath: getLab3TerminalPath(config?.lab3TerminalUrl),
|
||||||
|
username: config?.lab3Username?.trim() || LAB3_DEFAULT_USERNAME,
|
||||||
|
workingDirectory:
|
||||||
|
config?.lab3WorkingDirectory?.trim() || LAB3_DEFAULT_WORKING_DIRECTORY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLab3RuntimeConfig() {
|
||||||
|
const response = await fetch(LAB3_RUNTIME_CONFIG_PATH, { cache: "no-store" });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Runtime config request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = (await response.json()) as Lab3RuntimeConfig;
|
||||||
|
return normalizeLab3RuntimeConfig(config);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user